diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 15606a5bdd971281657543293db0bcdafca26fc4..b0d92907b11ec36a6dd9ff8414590c9467c38dcf 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,4 +16,3 @@ run_tests:
     when: always
     reports:
       junit: report.xml
-
diff --git a/go.mod b/go.mod
index 94c2cfbdd2c37f7bbedc4bcc6f91bd752bd0b492..f53323bb290e119feda2d24ebe935d02cfdc9e38 100644
--- a/go.mod
+++ b/go.mod
@@ -3,26 +3,36 @@ module git.perx.ru/perxis/perxis-go
 go 1.18
 
 require (
+	github.com/antonmedv/expr v1.9.0
 	github.com/go-kit/kit v0.12.0
 	github.com/golang/protobuf v1.5.2
+	github.com/gosimple/slug v1.13.1
 	github.com/hashicorp/go-multierror v1.1.1
+	github.com/hashicorp/golang-lru v0.5.4
 	github.com/pkg/errors v0.9.1
 	github.com/rs/xid v1.4.0
-	github.com/stretchr/testify v1.7.0
-	golang.org/x/net v0.0.0-20210917221730-978cfadd31cf
+	github.com/stretchr/testify v1.8.0
+	go.mongodb.org/mongo-driver v1.11.4
+	go.uber.org/zap v1.19.1
+	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
+	golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
 	google.golang.org/grpc v1.45.0
 	google.golang.org/protobuf v1.28.0
+	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
-	github.com/davecgh/go-spew v1.1.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-kit/log v0.2.0 // indirect
 	github.com/go-logfmt/logfmt v0.5.1 // indirect
+	github.com/google/go-cmp v0.5.7 // indirect
+	github.com/gosimple/unidecode v1.0.1 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/stretchr/objx v0.1.0 // indirect
-	golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect
+	github.com/stretchr/objx v0.4.0 // indirect
+	go.uber.org/atomic v1.9.0 // indirect
+	go.uber.org/multierr v1.7.0 // indirect
+	golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
 	golang.org/x/text v0.3.7 // indirect
 	google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
 )
diff --git a/go.sum b/go.sum
index 2dc3ca61e747e97abd24d179acc4e32112c07a4e..9dea84ab8c222f13374d0a59a405fa290feed61a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,13 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
+github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -14,8 +19,12 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH
 github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -23,6 +32,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
+github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4=
 github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=
@@ -46,42 +57,99 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q=
+github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
+github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
+github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
+github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
 github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
+github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+go.mongodb.org/mongo-driver v1.11.4 h1:4ayjakA013OdpGyL2K3ZqylTac/rMjrJOMZ1EHizXas=
+go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
+go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
+go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
+go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
+go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -94,8 +162,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ=
-golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -106,16 +174,24 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw=
-golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -162,11 +238,18 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go
new file mode 100644
index 0000000000000000000000000000000000000000..7f7248e5208f1691d7e094eaf640f361383faab6
--- /dev/null
+++ b/pkg/cache/cache.go
@@ -0,0 +1,90 @@
+package cache
+
+import (
+	"errors"
+	"fmt"
+	"time"
+
+	lru "github.com/hashicorp/golang-lru"
+	"go.uber.org/zap"
+)
+
+const (
+	defaultCacheSize = 1000
+	defaultTTL       = 30 * time.Second
+)
+
+var ErrNotFound = errors.New("not found")
+
+type Cache struct {
+	cache  *lru.Cache
+	ttl    time.Duration
+	logger *zap.Logger
+}
+
+type item struct {
+	value     interface{}
+	expiredAt time.Time
+}
+
+func NewCache(size int, ttl time.Duration, opts ...interface{}) *Cache {
+	if size == 0 {
+		size = defaultCacheSize
+	}
+	if ttl == 0 {
+		ttl = defaultTTL
+	}
+	c, err := lru.New(size)
+	if err != nil {
+		panic(err)
+	}
+	ch := &Cache{
+		cache:  c,
+		ttl:    ttl,
+		logger: zap.NewNop(),
+	}
+
+	for _, o := range opts {
+		switch p := o.(type) {
+		case *zap.Logger:
+			ch.logger = p
+		}
+	}
+
+	ch.logger = ch.logger.Named("Cache")
+
+	return ch
+}
+
+func (c *Cache) Set(key, value interface{}) (err error) {
+	c.cache.Add(key, &item{value: value, expiredAt: time.Now().Add(c.ttl)})
+	c.logger.Debug("Set", zap.String("key", fmt.Sprintf("%v", key)), zap.String("ptr", fmt.Sprintf("%p", value)))
+	return nil
+}
+
+func (c *Cache) Get(key interface{}) (value interface{}, err error) {
+	val, ok := c.cache.Get(key)
+	if ok {
+		v := val.(*item)
+		if v.expiredAt.Before(time.Now()) {
+			c.Remove(key)
+			c.logger.Debug("Expired", zap.String("key", fmt.Sprintf("%v", key)), zap.String("ptr", fmt.Sprintf("%p", v.value)))
+			return nil, ErrNotFound
+		}
+		c.logger.Debug("Hit", zap.String("key", fmt.Sprintf("%v", key)), zap.String("ptr", fmt.Sprintf("%p", v.value)))
+		return v.value, nil
+	}
+	c.logger.Debug("Miss", zap.String("key", fmt.Sprintf("%v", key)))
+	return nil, ErrNotFound
+}
+
+func (c *Cache) Remove(key interface{}) (err error) {
+	present := c.cache.Remove(key)
+	c.logger.Debug("Remove", zap.String("key", fmt.Sprintf("%v", key)))
+
+	if !present {
+		err = ErrNotFound
+	}
+
+	return
+}
diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..345a391c09044b61ebac4f869ba26bbcd9def13d
--- /dev/null
+++ b/pkg/cache/cache_test.go
@@ -0,0 +1,82 @@
+package cache
+
+import (
+	"errors"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCache(t *testing.T) {
+
+	t.Run("Simple", func(t *testing.T) {
+		c := NewCache(10, 0)
+
+		{
+			val, err := c.Get("test_key")
+			require.Error(t, err)
+			assert.True(t, errors.Is(err, ErrNotFound))
+			assert.Nil(t, val)
+		}
+		{
+			err := c.Set("test_key", "test_val")
+			require.NoError(t, err)
+
+			val, err := c.Get("test_key")
+			require.NoError(t, err)
+			assert.Equal(t, "test_val", val.(string))
+		}
+
+		{
+			err := c.Remove("test_key")
+			require.NoError(t, err)
+
+			val, err := c.Get("test_key")
+			assert.True(t, errors.Is(err, ErrNotFound))
+			assert.Nil(t, val)
+		}
+	})
+	t.Run("Value Evicted", func(t *testing.T) {
+		c := NewCache(1, 0)
+
+		{
+			err := c.Set("test_key_1", "test_val_1")
+			require.NoError(t, err)
+
+			val, err := c.Get("test_key_1")
+			require.NoError(t, err)
+			assert.Equal(t, "test_val_1", val.(string))
+		}
+
+		{
+			err := c.Set("test_key_2", "test_val_2")
+			require.NoError(t, err)
+
+			val, err := c.Get("test_key_1")
+			assert.True(t, errors.Is(err, ErrNotFound))
+			assert.Nil(t, val)
+			val, err = c.Get("test_key_2")
+			require.NoError(t, err)
+			assert.Equal(t, "test_val_2", val.(string))
+		}
+
+	})
+	t.Run("TTL expired", func(t *testing.T) {
+		c := NewCache(10, 10*time.Millisecond)
+
+		err := c.Set("test_key", "test_val")
+		require.NoError(t, err)
+
+		val, err := c.Get("test_key")
+		require.NoError(t, err)
+		assert.Equal(t, "test_val", val.(string))
+
+		time.Sleep(15 * time.Millisecond)
+
+		val, err = c.Get("test_key")
+		assert.True(t, errors.Is(err, ErrNotFound))
+		assert.Nil(t, val)
+	})
+}
diff --git a/pkg/clients/client.go b/pkg/clients/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..f38b5acc9b442be39139523bcd316947d5d54fe7
--- /dev/null
+++ b/pkg/clients/client.go
@@ -0,0 +1,87 @@
+package clients
+
+// Client - приложение имеющее доступ к API
+type Client struct {
+	// Внутренний идентификатор клиента внутри системы
+	ID string `json:"id" bson:"_id"`
+
+	// Идентификатор пространства
+	SpaceID string `json:"space_id" bson:"-"`
+
+	// Имя приложения (обязательное поле)
+	Name string `json:"name" bson:"name"`
+
+	// Параметры аутентификации клиента
+	OAuth  *OAuth  `json:"oauth,omitempty" bson:"oauth,omitempty"`
+	TLS    *TLS    `json:"tls,omitempty" bson:"tls,omitempty"`
+	APIKey *APIKey `json:"api_key,omitempty" bson:"api_key,omitempty"`
+
+	// Описание клиента, назначение
+	Description string `json:"description" bson:"description"`
+
+	// Приложение отключено и не может авторизоваться
+	Disabled *bool `json:"disabled,omitempty" bson:"disabled,omitempty"`
+
+	// Роль приложения в пространстве
+	RoleID string `json:"role_id" bson:"role_id"`
+}
+
+type OAuth struct {
+	ClientID     string `bson:"client_id,omitempty" json:"client_id,omitempty"`         // Идентификатор клиента выданные IdP сервером, используется для идентификации клиента
+	AuthID       string `bson:"auth_id,omitempty" json:"auth_id,omitempty"`             // Сервис, который используется для авторизации клиента
+	TokenURL     string `bson:"token_url,omitempty" json:"token_url,omitempty"`         // URL для получения/обновления access token клиента (опционально)
+	ClientSecret string `bson:"client_secret,omitempty" json:"client_secret,omitempty"` // Секретный Ключ клиента, используется для идентификации клиента (опционально)
+}
+
+type APIKey struct {
+	Key    string `bson:"key,omitempty" json:"key,omitempty"`
+	Rotate bool   `bson:"-" json:"rotate,omitempty"`
+}
+
+type TLS struct {
+	Subject string `json:"subject,omitempty"`
+	CACert  string `json:"ca_cert,omitempty"`
+	Cert    string `json:"cert,omitempty"`
+	Key     string `json:"key,omitempty"`
+}
+
+func (c *Client) SetDisabled(b bool) *Client {
+	c.Disabled = &b
+	return c
+}
+
+func (c *Client) IsDisabled() bool {
+	if c.Disabled != nil && *c.Disabled {
+		return true
+	}
+	return false
+}
+
+func (c Client) Clone() *Client {
+	clone := &Client{
+		ID:          c.ID,
+		SpaceID:     c.SpaceID,
+		Name:        c.Name,
+		Description: c.Description,
+		RoleID:      c.RoleID,
+	}
+
+	if c.OAuth != nil {
+		temp := *c.OAuth
+		clone.OAuth = &temp
+	}
+	if c.TLS != nil {
+		temp := *c.TLS
+		clone.TLS = &temp
+	}
+	if c.APIKey != nil {
+		temp := *c.APIKey
+		clone.APIKey = &temp
+	}
+	if c.Disabled != nil {
+		temp := *c.Disabled
+		clone.Disabled = &temp
+	}
+
+	return clone
+}
diff --git a/pkg/clients/mocks/Clients.go b/pkg/clients/mocks/Clients.go
new file mode 100644
index 0000000000000000000000000000000000000000..bfeb7e946a1fe50d044479ba785aac15f484ac31
--- /dev/null
+++ b/pkg/clients/mocks/Clients.go
@@ -0,0 +1,149 @@
+// Code generated by mockery v2.7.4. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	clients "git.perx.ru/perxis/perxis-go/pkg/clients"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Clients is an autogenerated mock type for the Clients type
+type Clients struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, client
+func (_m *Clients) Create(ctx context.Context, client *clients.Client) (*clients.Client, error) {
+	ret := _m.Called(ctx, client)
+
+	var r0 *clients.Client
+	if rf, ok := ret.Get(0).(func(context.Context, *clients.Client) *clients.Client); ok {
+		r0 = rf(ctx, client)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*clients.Client)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *clients.Client) error); ok {
+		r1 = rf(ctx, client)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, spaceId, id
+func (_m *Clients) Delete(ctx context.Context, spaceId string, id string) error {
+	ret := _m.Called(ctx, spaceId, id)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceId, id)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Enable provides a mock function with given fields: ctx, spaceId, id, enable
+func (_m *Clients) Enable(ctx context.Context, spaceId string, id string, enable bool) error {
+	ret := _m.Called(ctx, spaceId, id, enable)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) error); ok {
+		r0 = rf(ctx, spaceId, id, enable)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Get provides a mock function with given fields: ctx, spaceId, id
+func (_m *Clients) Get(ctx context.Context, spaceId string, id string) (*clients.Client, error) {
+	ret := _m.Called(ctx, spaceId, id)
+
+	var r0 *clients.Client
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) *clients.Client); ok {
+		r0 = rf(ctx, spaceId, id)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*clients.Client)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
+		r1 = rf(ctx, spaceId, id)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetBy provides a mock function with given fields: ctx, spaceId, params
+func (_m *Clients) GetBy(ctx context.Context, spaceId string, params *clients.GetByParams) (*clients.Client, error) {
+	ret := _m.Called(ctx, spaceId, params)
+
+	var r0 *clients.Client
+	if rf, ok := ret.Get(0).(func(context.Context, string, *clients.GetByParams) *clients.Client); ok {
+		r0 = rf(ctx, spaceId, params)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*clients.Client)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, *clients.GetByParams) error); ok {
+		r1 = rf(ctx, spaceId, params)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// List provides a mock function with given fields: ctx, spaceId
+func (_m *Clients) List(ctx context.Context, spaceId string) ([]*clients.Client, error) {
+	ret := _m.Called(ctx, spaceId)
+
+	var r0 []*clients.Client
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*clients.Client); ok {
+		r0 = rf(ctx, spaceId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*clients.Client)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, spaceId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Update provides a mock function with given fields: ctx, client
+func (_m *Clients) Update(ctx context.Context, client *clients.Client) error {
+	ret := _m.Called(ctx, client)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *clients.Client) error); ok {
+		r0 = rf(ctx, client)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
diff --git a/pkg/clients/service.go b/pkg/clients/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..823de82bdbe720e2a9ff24f069cb94458bf1cf25
--- /dev/null
+++ b/pkg/clients/service.go
@@ -0,0 +1,37 @@
+package clients
+
+import (
+	"context"
+)
+
+type GetByParams struct {
+	OAuthClientID string `json:"oauth_client_id,omitempty"`
+	APIKey        string `json:"api_key,omitempty"`
+	TLSSubject    string `json:"tls_subject,omitempty"`
+}
+
+// @microgen grpc, recovering, middleware
+// @protobuf git.perx.ru/perxis/perxis-go/proto/clients
+// @grpc-addr content.clients.Clients
+type Clients interface {
+	// Create - создает клиента (приложение) для работы с API
+	Create(ctx context.Context, client *Client) (created *Client, err error)
+
+	// Get - возвращает клиента по id
+	Get(ctx context.Context, spaceId, id string) (client *Client, err error)
+
+	// GetBy - возвращает клиента по идентификатору системы авторизации
+	GetBy(ctx context.Context, spaceId string, params *GetByParams) (client *Client, err error)
+
+	// List - возвращает список клиентов созданных в пространстве
+	List(ctx context.Context, spaceId string) (clients []*Client, err error)
+
+	// Update - обновляет параметры клиента
+	Update(ctx context.Context, client *Client) (err error)
+
+	// Delete - удаляет указанного клиента из пространстве
+	Delete(ctx context.Context, spaceId, id string) (err error)
+
+	// Enable - активирует/деактивирует клиента. Клиент не сможет обращаться к API платформы
+	Enable(ctx context.Context, spaceId, id string, enable bool) (err error)
+}
diff --git a/pkg/clients/transport/client.microgen.go b/pkg/clients/transport/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..633855af4d1f7fe74260ec7b4ece23db6063ebfb
--- /dev/null
+++ b/pkg/clients/transport/client.microgen.go
@@ -0,0 +1,108 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	"errors"
+	clients "git.perx.ru/perxis/perxis-go/pkg/clients"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+func (set EndpointsSet) Create(arg0 context.Context, arg1 *clients.Client) (res0 *clients.Client, res1 error) {
+	request := CreateRequest{Client: arg1}
+	response, res1 := set.CreateEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*CreateResponse).Created, res1
+}
+
+func (set EndpointsSet) Get(arg0 context.Context, arg1 string, arg2 string) (res0 *clients.Client, res1 error) {
+	request := GetRequest{
+		Id:      arg2,
+		SpaceId: arg1,
+	}
+	response, res1 := set.GetEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*GetResponse).Client, res1
+}
+
+func (set EndpointsSet) GetBy(arg0 context.Context, arg1 string, arg2 *clients.GetByParams) (res0 *clients.Client, res1 error) {
+	request := GetByRequest{
+		Config:  arg2,
+		SpaceId: arg1,
+	}
+	response, res1 := set.GetByEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*GetByResponse).Client, res1
+}
+
+func (set EndpointsSet) List(arg0 context.Context, arg1 string) (res0 []*clients.Client, res1 error) {
+	request := ListRequest{SpaceId: arg1}
+	response, res1 := set.ListEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*ListResponse).Clients, res1
+}
+
+func (set EndpointsSet) Update(arg0 context.Context, arg1 *clients.Client) (res0 error) {
+	request := UpdateRequest{Client: arg1}
+	_, res0 = set.UpdateEndpoint(arg0, &request)
+	if res0 != nil {
+		if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res0 = errors.New(e.Message())
+		}
+		return
+	}
+	return res0
+}
+
+func (set EndpointsSet) Delete(arg0 context.Context, arg1 string, arg2 string) (res0 error) {
+	request := DeleteRequest{
+		Id:      arg2,
+		SpaceId: arg1,
+	}
+	_, res0 = set.DeleteEndpoint(arg0, &request)
+	if res0 != nil {
+		if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res0 = errors.New(e.Message())
+		}
+		return
+	}
+	return res0
+}
+
+func (set EndpointsSet) Enable(arg0 context.Context, arg1 string, arg2 string, arg3 bool) (res0 error) {
+	request := EnableRequest{
+		Enable:  arg3,
+		Id:      arg2,
+		SpaceId: arg1,
+	}
+	_, res0 = set.EnableEndpoint(arg0, &request)
+	if res0 != nil {
+		if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res0 = errors.New(e.Message())
+		}
+		return
+	}
+	return res0
+}
diff --git a/pkg/clients/transport/endpoints.microgen.go b/pkg/clients/transport/endpoints.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..bf73c784e68eb95f1dc631184046a4016bcca7a1
--- /dev/null
+++ b/pkg/clients/transport/endpoints.microgen.go
@@ -0,0 +1,16 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import endpoint "github.com/go-kit/kit/endpoint"
+
+// EndpointsSet implements Clients API and used for transport purposes.
+type EndpointsSet struct {
+	CreateEndpoint endpoint.Endpoint
+	GetEndpoint    endpoint.Endpoint
+	GetByEndpoint  endpoint.Endpoint
+	ListEndpoint   endpoint.Endpoint
+	UpdateEndpoint endpoint.Endpoint
+	DeleteEndpoint endpoint.Endpoint
+	EnableEndpoint endpoint.Endpoint
+}
diff --git a/pkg/clients/transport/exchanges.microgen.go b/pkg/clients/transport/exchanges.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..2a1a8e65967661b117a20ff62fc6e20837afce2b
--- /dev/null
+++ b/pkg/clients/transport/exchanges.microgen.go
@@ -0,0 +1,58 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import clients "git.perx.ru/perxis/perxis-go/pkg/clients"
+
+type (
+	CreateRequest struct {
+		Client *clients.Client `json:"client"`
+	}
+	CreateResponse struct {
+		Created *clients.Client `json:"created"`
+	}
+
+	GetRequest struct {
+		SpaceId string `json:"space_id"`
+		Id      string `json:"id"`
+	}
+	GetResponse struct {
+		Client *clients.Client `json:"client"`
+	}
+
+	GetByRequest struct {
+		SpaceId string               `json:"space_id"`
+		Config  *clients.GetByParams `json:"config"`
+	}
+	GetByResponse struct {
+		Client *clients.Client `json:"client"`
+	}
+
+	ListRequest struct {
+		SpaceId string `json:"space_id"`
+	}
+	ListResponse struct {
+		Clients []*clients.Client `json:"clients"`
+	}
+
+	UpdateRequest struct {
+		Client *clients.Client `json:"client"`
+	}
+	// Formal exchange type, please do not delete.
+	UpdateResponse struct{}
+
+	DeleteRequest struct {
+		SpaceId string `json:"space_id"`
+		Id      string `json:"id"`
+	}
+	// Formal exchange type, please do not delete.
+	DeleteResponse struct{}
+
+	EnableRequest struct {
+		SpaceId string `json:"space_id"`
+		Id      string `json:"id"`
+		Enable  bool   `json:"enable"`
+	}
+	// Formal exchange type, please do not delete.
+	EnableResponse struct{}
+)
diff --git a/pkg/clients/transport/grpc/client.microgen.go b/pkg/clients/transport/grpc/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..421a0178c29b691c33085761bc1710e3c4cbe4eb
--- /dev/null
+++ b/pkg/clients/transport/grpc/client.microgen.go
@@ -0,0 +1,68 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/clients/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/clients"
+	grpckit "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	grpc "google.golang.org/grpc"
+)
+
+func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet {
+	if addr == "" {
+		addr = "content.clients.Clients"
+	}
+	return transport.EndpointsSet{
+		CreateEndpoint: grpckit.NewClient(
+			conn, addr, "Create",
+			_Encode_Create_Request,
+			_Decode_Create_Response,
+			pb.CreateResponse{},
+			opts...,
+		).Endpoint(),
+		DeleteEndpoint: grpckit.NewClient(
+			conn, addr, "Delete",
+			_Encode_Delete_Request,
+			_Decode_Delete_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+		EnableEndpoint: grpckit.NewClient(
+			conn, addr, "Enable",
+			_Encode_Enable_Request,
+			_Decode_Enable_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+		GetByEndpoint: grpckit.NewClient(
+			conn, addr, "GetBy",
+			_Encode_GetBy_Request,
+			_Decode_GetBy_Response,
+			pb.GetByResponse{},
+			opts...,
+		).Endpoint(),
+		GetEndpoint: grpckit.NewClient(
+			conn, addr, "Get",
+			_Encode_Get_Request,
+			_Decode_Get_Response,
+			pb.GetResponse{},
+			opts...,
+		).Endpoint(),
+		ListEndpoint: grpckit.NewClient(
+			conn, addr, "List",
+			_Encode_List_Request,
+			_Decode_List_Response,
+			pb.ListResponse{},
+			opts...,
+		).Endpoint(),
+		UpdateEndpoint: grpckit.NewClient(
+			conn, addr, "Update",
+			_Encode_Update_Request,
+			_Decode_Update_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+	}
+}
diff --git a/pkg/clients/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/clients/transport/grpc/protobuf_endpoint_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..fea7765facc0f5ba2c6f07a5c7016d2e326a5d23
--- /dev/null
+++ b/pkg/clients/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -0,0 +1,295 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// Please, do not change functions names!
+package transportgrpc
+
+import (
+	"context"
+	"errors"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	transport "git.perx.ru/perxis/perxis-go/pkg/clients/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/clients"
+	empty "github.com/golang/protobuf/ptypes/empty"
+)
+
+func _Encode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetRequest")
+	}
+	req := request.(*transport.GetRequest)
+	return &pb.GetRequest{
+		Id:      req.Id,
+		SpaceId: req.SpaceId,
+	}, nil
+}
+
+func _Encode_List_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListRequest")
+	}
+	req := request.(*transport.ListRequest)
+	return &pb.ListRequest{SpaceId: req.SpaceId}, nil
+}
+
+func _Encode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil DeleteRequest")
+	}
+	req := request.(*transport.DeleteRequest)
+	return &pb.DeleteRequest{
+		Id:      req.Id,
+		SpaceId: req.SpaceId,
+	}, nil
+}
+
+func _Encode_Enable_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil EnableRequest")
+	}
+	req := request.(*transport.EnableRequest)
+	return &pb.EnableRequest{
+		Enable:  req.Enable,
+		Id:      req.Id,
+		SpaceId: req.SpaceId,
+	}, nil
+}
+
+func _Encode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetResponse")
+	}
+	resp := response.(*transport.GetResponse)
+	respClient, err := PtrClientToProto(resp.Client)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.GetResponse{Client: respClient}, nil
+}
+
+func _Encode_List_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListResponse")
+	}
+	resp := response.(*transport.ListResponse)
+	respClients, err := ListPtrClientToProto(resp.Clients)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.ListResponse{Clients: respClients}, nil
+}
+
+func _Encode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Encode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Encode_Enable_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetRequest")
+	}
+	req := request.(*pb.GetRequest)
+	return &transport.GetRequest{
+		Id:      string(req.Id),
+		SpaceId: string(req.SpaceId),
+	}, nil
+}
+
+func _Decode_List_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListRequest")
+	}
+	req := request.(*pb.ListRequest)
+	return &transport.ListRequest{SpaceId: string(req.SpaceId)}, nil
+}
+
+func _Decode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil DeleteRequest")
+	}
+	req := request.(*pb.DeleteRequest)
+	return &transport.DeleteRequest{
+		Id:      string(req.Id),
+		SpaceId: string(req.SpaceId),
+	}, nil
+}
+
+func _Decode_Enable_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil EnableRequest")
+	}
+	req := request.(*pb.EnableRequest)
+	return &transport.EnableRequest{
+		Enable:  bool(req.Enable),
+		Id:      string(req.Id),
+		SpaceId: string(req.SpaceId),
+	}, nil
+}
+
+func _Decode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetResponse")
+	}
+	resp := response.(*pb.GetResponse)
+	respClient, err := ProtoToPtrClient(resp.Client)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.GetResponse{Client: respClient}, nil
+}
+
+func _Decode_List_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListResponse")
+	}
+	resp := response.(*pb.ListResponse)
+	respClients, err := ProtoToListPtrClient(resp.Clients)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.ListResponse{Clients: respClients}, nil
+}
+
+func _Decode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_Enable_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Encode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil CreateRequest")
+	}
+	req := request.(*transport.CreateRequest)
+	reqClient, err := PtrClientToProto(req.Client)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateRequest{Client: reqClient}, nil
+}
+
+func _Encode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil UpdateRequest")
+	}
+	req := request.(*transport.UpdateRequest)
+	reqClient, err := PtrClientToProto(req.Client)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.UpdateRequest{Client: reqClient}, nil
+}
+
+func _Encode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil CreateResponse")
+	}
+	resp := response.(*transport.CreateResponse)
+	respCreated, err := PtrClientToProto(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateResponse{Created: respCreated}, nil
+}
+
+func _Decode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil CreateRequest")
+	}
+	req := request.(*pb.CreateRequest)
+	reqClient, err := ProtoToPtrClient(req.Client)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateRequest{Client: reqClient}, nil
+}
+
+func _Decode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil UpdateRequest")
+	}
+	req := request.(*pb.UpdateRequest)
+	reqClient, err := ProtoToPtrClient(req.Client)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.UpdateRequest{Client: reqClient}, nil
+}
+
+func _Decode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil CreateResponse")
+	}
+	resp := response.(*pb.CreateResponse)
+	respCreated, err := ProtoToPtrClient(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateResponse{Created: respCreated}, nil
+}
+
+func _Encode_GetBy_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetByRequest")
+	}
+	req := request.(*transport.GetByRequest)
+	pbreq := &pb.GetByRequest{SpaceId: req.SpaceId}
+	if req != nil && req.Config != nil {
+		pbreq.ApiKey = req.Config.APIKey
+		pbreq.TlsSubject = req.Config.TLSSubject
+		pbreq.OauthClientId = req.Config.OAuthClientID
+	}
+	return pbreq, nil
+}
+
+func _Encode_GetBy_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetByResponse")
+	}
+	resp := response.(*transport.GetByResponse)
+	respClient, err := PtrClientToProto(resp.Client)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.GetByResponse{Client: respClient}, nil
+}
+
+func _Decode_GetBy_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetByRequest")
+	}
+	req := request.(*pb.GetByRequest)
+	return &transport.GetByRequest{
+		Config: &clients.GetByParams{
+			OAuthClientID: req.OauthClientId,
+			APIKey:        req.ApiKey,
+			TLSSubject:    req.TlsSubject,
+		},
+		SpaceId: string(req.SpaceId),
+	}, nil
+}
+
+func _Decode_GetBy_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetByResponse")
+	}
+	resp := response.(*pb.GetByResponse)
+	respClient, err := ProtoToPtrClient(resp.Client)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.GetByResponse{Client: respClient}, nil
+}
diff --git a/pkg/clients/transport/grpc/protobuf_type_converters.microgen.go b/pkg/clients/transport/grpc/protobuf_type_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..5212c0b5d2dc9c96533f5f64aee9ed58ab241b41
--- /dev/null
+++ b/pkg/clients/transport/grpc/protobuf_type_converters.microgen.go
@@ -0,0 +1,164 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// It is better for you if you do not change functions names!
+// This file will never be overwritten.
+package transportgrpc
+
+import (
+	service "git.perx.ru/perxis/perxis-go/pkg/clients"
+	permission "git.perx.ru/perxis/perxis-go/pkg/permission"
+	pb "git.perx.ru/perxis/perxis-go/proto/clients"
+	commonpb "git.perx.ru/perxis/perxis-go/proto/common"
+)
+
+func ListStringToProto(environments []string) ([]string, error) {
+	return environments, nil
+}
+
+func ProtoToListString(protoEnvironments []string) ([]string, error) {
+	return protoEnvironments, nil
+}
+
+func PtrClientToProto(client *service.Client) (*pb.Client, error) {
+	if client == nil {
+		return nil, nil
+	}
+
+	var oauth *pb.Client_OAuth
+	var tls *pb.Client_TLS
+	var apikey *pb.Client_APIKey
+
+	if client.OAuth != nil {
+		oauth = &pb.Client_OAuth{
+			ClientId:     client.OAuth.ClientID,
+			AuthId:       client.OAuth.AuthID,
+			TokenUrl:     client.OAuth.TokenURL,
+			ClientSecret: client.OAuth.ClientSecret,
+		}
+	}
+	if client.TLS != nil {
+		tls = &pb.Client_TLS{
+			Subject: client.TLS.Subject,
+		}
+	}
+	if client.APIKey != nil {
+		apikey = &pb.Client_APIKey{
+			Key:    client.APIKey.Key,
+			Rotate: client.APIKey.Rotate,
+		}
+	}
+
+	return &pb.Client{
+		Id:          client.ID,
+		SpaceId:     client.SpaceID,
+		Name:        client.Name,
+		Description: client.Description,
+		Disabled:    client.Disabled,
+		RoleId:      client.RoleID,
+		//Environments: client.Environments,
+		//Rules:        rules,
+		Oauth:  oauth,
+		Tls:    tls,
+		ApiKey: apikey,
+	}, nil
+}
+
+func ProtoToPtrClient(protoClient *pb.Client) (*service.Client, error) {
+	if protoClient == nil {
+		return nil, nil
+	}
+
+	var oauth *service.OAuth
+	var tls *service.TLS
+	var apikey *service.APIKey
+
+	if protoClient.Oauth != nil {
+		oauth = &service.OAuth{
+			ClientID:     protoClient.Oauth.ClientId,
+			AuthID:       protoClient.Oauth.AuthId,
+			TokenURL:     protoClient.Oauth.TokenUrl,
+			ClientSecret: protoClient.Oauth.ClientSecret,
+		}
+	}
+	if protoClient.Tls != nil {
+		tls = &service.TLS{
+			Subject: protoClient.Tls.Subject,
+		}
+	}
+	if protoClient.ApiKey != nil {
+		apikey = &service.APIKey{
+			Key:    protoClient.ApiKey.Key,
+			Rotate: protoClient.ApiKey.Rotate,
+		}
+	}
+
+	return &service.Client{
+		ID:          protoClient.Id,
+		SpaceID:     protoClient.SpaceId,
+		Name:        protoClient.Name,
+		Description: protoClient.Description,
+		Disabled:    protoClient.Disabled,
+		RoleID:      protoClient.RoleId,
+		OAuth:       oauth,
+		TLS:         tls,
+		APIKey:      apikey,
+	}, nil
+}
+
+func ListPtrClientToProto(clients []*service.Client) ([]*pb.Client, error) {
+	protoClients := make([]*pb.Client, 0, len(clients))
+	for _, c := range clients {
+		protoClient, _ := PtrClientToProto(c)
+		protoClients = append(protoClients, protoClient)
+	}
+	return protoClients, nil
+}
+
+func ProtoToListPtrClient(protoClients []*pb.Client) ([]*service.Client, error) {
+	clients := make([]*service.Client, 0, len(protoClients))
+	for _, c := range protoClients {
+		client, _ := ProtoToPtrClient(c)
+		clients = append(clients, client)
+	}
+	return clients, nil
+}
+
+func PtrPermissionRuleToProto(rule *permission.Rule) (*commonpb.Rule, error) {
+	if rule == nil {
+		return nil, nil
+	}
+	actions := make([]commonpb.Action, 0, len(rule.Actions))
+	for _, a := range rule.Actions {
+		actions = append(actions, commonpb.Action(a))
+	}
+	return &commonpb.Rule{
+		CollectionId:    rule.CollectionID,
+		Actions:         actions,
+		Access:          commonpb.Access(rule.Access),
+		HiddenFields:    rule.HiddenFields,
+		ReadonlyFields:  rule.ReadonlyFields,
+		WriteonlyFields: rule.WriteonlyFields,
+		ReadFilter:      rule.ReadFilter,
+		WriteFilter:     rule.WriteFilter,
+	}, nil
+}
+
+func ProtoToPtrPermissionRule(protoRule *commonpb.Rule) (*permission.Rule, error) {
+	if protoRule == nil {
+		return nil, nil
+	}
+	actions := make([]permission.Action, 0, len(protoRule.Actions))
+	for _, a := range protoRule.Actions {
+		actions = append(actions, permission.Action(a))
+	}
+	return &permission.Rule{
+		CollectionID:    protoRule.CollectionId,
+		Actions:         actions,
+		Access:          permission.Access(protoRule.Access),
+		HiddenFields:    protoRule.HiddenFields,
+		ReadonlyFields:  protoRule.ReadonlyFields,
+		WriteonlyFields: protoRule.WriteonlyFields,
+		ReadFilter:      protoRule.ReadFilter,
+		WriteFilter:     protoRule.WriteFilter,
+	}, nil
+}
diff --git a/pkg/clients/transport/grpc/server.microgen.go b/pkg/clients/transport/grpc/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..7408e691dd4fb1e8e772ffe9e530fcd112d02729
--- /dev/null
+++ b/pkg/clients/transport/grpc/server.microgen.go
@@ -0,0 +1,127 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// DO NOT EDIT.
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/clients/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/clients"
+	grpc "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	context "golang.org/x/net/context"
+)
+
+type clientsServer struct {
+	create grpc.Handler
+	get    grpc.Handler
+	getBy  grpc.Handler
+	list   grpc.Handler
+	update grpc.Handler
+	delete grpc.Handler
+	enable grpc.Handler
+
+	pb.UnimplementedClientsServer
+}
+
+func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.ClientsServer {
+	return &clientsServer{
+		create: grpc.NewServer(
+			endpoints.CreateEndpoint,
+			_Decode_Create_Request,
+			_Encode_Create_Response,
+			opts...,
+		),
+		delete: grpc.NewServer(
+			endpoints.DeleteEndpoint,
+			_Decode_Delete_Request,
+			_Encode_Delete_Response,
+			opts...,
+		),
+		enable: grpc.NewServer(
+			endpoints.EnableEndpoint,
+			_Decode_Enable_Request,
+			_Encode_Enable_Response,
+			opts...,
+		),
+		get: grpc.NewServer(
+			endpoints.GetEndpoint,
+			_Decode_Get_Request,
+			_Encode_Get_Response,
+			opts...,
+		),
+		getBy: grpc.NewServer(
+			endpoints.GetByEndpoint,
+			_Decode_GetBy_Request,
+			_Encode_GetBy_Response,
+			opts...,
+		),
+		list: grpc.NewServer(
+			endpoints.ListEndpoint,
+			_Decode_List_Request,
+			_Encode_List_Response,
+			opts...,
+		),
+		update: grpc.NewServer(
+			endpoints.UpdateEndpoint,
+			_Decode_Update_Request,
+			_Encode_Update_Response,
+			opts...,
+		),
+	}
+}
+
+func (S *clientsServer) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) {
+	_, resp, err := S.create.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.CreateResponse), nil
+}
+
+func (S *clientsServer) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) {
+	_, resp, err := S.get.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.GetResponse), nil
+}
+
+func (S *clientsServer) GetBy(ctx context.Context, req *pb.GetByRequest) (*pb.GetByResponse, error) {
+	_, resp, err := S.getBy.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.GetByResponse), nil
+}
+
+func (S *clientsServer) List(ctx context.Context, req *pb.ListRequest) (*pb.ListResponse, error) {
+	_, resp, err := S.list.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.ListResponse), nil
+}
+
+func (S *clientsServer) Update(ctx context.Context, req *pb.UpdateRequest) (*empty.Empty, error) {
+	_, resp, err := S.update.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
+
+func (S *clientsServer) Delete(ctx context.Context, req *pb.DeleteRequest) (*empty.Empty, error) {
+	_, resp, err := S.delete.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
+
+func (S *clientsServer) Enable(ctx context.Context, req *pb.EnableRequest) (*empty.Empty, error) {
+	_, resp, err := S.enable.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
diff --git a/pkg/clients/transport/server.microgen.go b/pkg/clients/transport/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..4c8b6a3e538dd5d1f28c3e7fd2b9f56af54cae8e
--- /dev/null
+++ b/pkg/clients/transport/server.microgen.go
@@ -0,0 +1,77 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	clients "git.perx.ru/perxis/perxis-go/pkg/clients"
+	endpoint "github.com/go-kit/kit/endpoint"
+)
+
+func Endpoints(svc clients.Clients) EndpointsSet {
+	return EndpointsSet{
+		CreateEndpoint: CreateEndpoint(svc),
+		DeleteEndpoint: DeleteEndpoint(svc),
+		EnableEndpoint: EnableEndpoint(svc),
+		GetByEndpoint:  GetByEndpoint(svc),
+		GetEndpoint:    GetEndpoint(svc),
+		ListEndpoint:   ListEndpoint(svc),
+		UpdateEndpoint: UpdateEndpoint(svc),
+	}
+}
+
+func CreateEndpoint(svc clients.Clients) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*CreateRequest)
+		res0, res1 := svc.Create(arg0, req.Client)
+		return &CreateResponse{Created: res0}, res1
+	}
+}
+
+func GetEndpoint(svc clients.Clients) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetRequest)
+		res0, res1 := svc.Get(arg0, req.SpaceId, req.Id)
+		return &GetResponse{Client: res0}, res1
+	}
+}
+
+func GetByEndpoint(svc clients.Clients) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetByRequest)
+		res0, res1 := svc.GetBy(arg0, req.SpaceId, req.Config)
+		return &GetByResponse{Client: res0}, res1
+	}
+}
+
+func ListEndpoint(svc clients.Clients) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*ListRequest)
+		res0, res1 := svc.List(arg0, req.SpaceId)
+		return &ListResponse{Clients: res0}, res1
+	}
+}
+
+func UpdateEndpoint(svc clients.Clients) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*UpdateRequest)
+		res0 := svc.Update(arg0, req.Client)
+		return &UpdateResponse{}, res0
+	}
+}
+
+func DeleteEndpoint(svc clients.Clients) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*DeleteRequest)
+		res0 := svc.Delete(arg0, req.SpaceId, req.Id)
+		return &DeleteResponse{}, res0
+	}
+}
+
+func EnableEndpoint(svc clients.Clients) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*EnableRequest)
+		res0 := svc.Enable(arg0, req.SpaceId, req.Id, req.Enable)
+		return &EnableResponse{}, res0
+	}
+}
diff --git a/pkg/collaborators/collaborator.go b/pkg/collaborators/collaborator.go
new file mode 100644
index 0000000000000000000000000000000000000000..701d8e85b578dafc2036c281c204720080412ac5
--- /dev/null
+++ b/pkg/collaborators/collaborator.go
@@ -0,0 +1,7 @@
+package collaborators
+
+type Collaborator struct {
+	SpaceID string `bson:"spaceId"`
+	Subject string `bson:"subject"`
+	Role    string `bson:"role"`
+}
diff --git a/pkg/collaborators/mocks/Collaborators.go b/pkg/collaborators/mocks/Collaborators.go
new file mode 100644
index 0000000000000000000000000000000000000000..6bcd7fb1c462ae85ef01ca50efa6145ce2f8d591
--- /dev/null
+++ b/pkg/collaborators/mocks/Collaborators.go
@@ -0,0 +1,110 @@
+// Code generated by mockery v2.7.4. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	collaborators "git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Collaborators is an autogenerated mock type for the Collaborators type
+type Collaborators struct {
+	mock.Mock
+}
+
+// Get provides a mock function with given fields: ctx, spaceId, userId
+func (_m *Collaborators) Get(ctx context.Context, spaceId, subject string) (string, error) {
+	ret := _m.Called(ctx, spaceId, subject)
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) string); ok {
+		r0 = rf(ctx, spaceId, subject)
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
+		r1 = rf(ctx, spaceId, subject)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// ListCollaborators provides a mock function with given fields: ctx, spaceId
+func (_m *Collaborators) ListCollaborators(ctx context.Context, spaceId string) ([]*collaborators.Collaborator, error) {
+	ret := _m.Called(ctx, spaceId)
+
+	var r0 []*collaborators.Collaborator
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*collaborators.Collaborator); ok {
+		r0 = rf(ctx, spaceId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*collaborators.Collaborator)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, spaceId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// ListSpaces provides a mock function with given fields: ctx, userId
+func (_m *Collaborators) ListSpaces(ctx context.Context, subject string) ([]*collaborators.Collaborator, error) {
+	ret := _m.Called(ctx, subject)
+
+	var r0 []*collaborators.Collaborator
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*collaborators.Collaborator); ok {
+		r0 = rf(ctx, subject)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*collaborators.Collaborator)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, subject)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Remove provides a mock function with given fields: ctx, spaceId, userId
+func (_m *Collaborators) Remove(ctx context.Context, spaceId, subject string) error {
+	ret := _m.Called(ctx, spaceId, subject)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceId, subject)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Set provides a mock function with given fields: ctx, spaceId, userId, role
+func (_m *Collaborators) Set(ctx context.Context, spaceId, subject, role string) error {
+	ret := _m.Called(ctx, spaceId, subject, role)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, subject, role)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
diff --git a/pkg/collaborators/service.go b/pkg/collaborators/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..fa56271b89e57a87fc501a11a1ae648f6214a7d5
--- /dev/null
+++ b/pkg/collaborators/service.go
@@ -0,0 +1,26 @@
+package collaborators
+
+import (
+	"context"
+)
+
+// @microgen grpc, recovering, middleware
+// @protobuf git.perx.ru/perxis/perxis-go/proto/collaborators
+// @grpc-addr content.collaborators.Collaborators
+type Collaborators interface {
+
+	// Set - устанавливает участие пользователя в пространстве и его роль
+	Set(ctx context.Context, spaceId, subject, role string) (err error)
+
+	// Get - возвращает роль пользователя в пространстве
+	Get(ctx context.Context, spaceId, subject string) (role string, err error)
+
+	// Remove - удаляет участие пользователя в пространстве
+	Remove(ctx context.Context, spaceId, subject string) (err error)
+
+	// ListCollaborators - возвращает участников пространства и их ролей
+	ListCollaborators(ctx context.Context, spaceId string) (collaborators []*Collaborator, err error)
+
+	// ListSpaces - возвращает список пространств пользователя
+	ListSpaces(ctx context.Context, subject string) (spaces []*Collaborator, err error)
+}
diff --git a/pkg/collaborators/transport/client.microgen.go b/pkg/collaborators/transport/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..eb0dbbe7bc1ea7bad07220fba5cdc6e3b1c9e503
--- /dev/null
+++ b/pkg/collaborators/transport/client.microgen.go
@@ -0,0 +1,82 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	"errors"
+
+	collaborators "git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+func (set EndpointsSet) Set(arg0 context.Context, arg1 string, arg2 string, arg3 string) (res0 error) {
+	request := SetRequest{
+		Role:    arg3,
+		SpaceId: arg1,
+		Subject: arg2,
+	}
+	_, res0 = set.SetEndpoint(arg0, &request)
+	if res0 != nil {
+		if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res0 = errors.New(e.Message())
+		}
+		return
+	}
+	return res0
+}
+
+func (set EndpointsSet) Get(arg0 context.Context, arg1 string, arg2 string) (res0 string, res1 error) {
+	request := GetRequest{
+		SpaceId: arg1,
+		Subject: arg2,
+	}
+	response, res1 := set.GetEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*GetResponse).Role, res1
+}
+
+func (set EndpointsSet) Remove(arg0 context.Context, arg1 string, arg2 string) (res0 error) {
+	request := RemoveRequest{
+		SpaceId: arg1,
+		Subject: arg2,
+	}
+	_, res0 = set.RemoveEndpoint(arg0, &request)
+	if res0 != nil {
+		if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res0 = errors.New(e.Message())
+		}
+		return
+	}
+	return res0
+}
+
+func (set EndpointsSet) ListCollaborators(arg0 context.Context, arg1 string) (res0 []*collaborators.Collaborator, res1 error) {
+	request := ListCollaboratorsRequest{SpaceId: arg1}
+	response, res1 := set.ListCollaboratorsEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*ListCollaboratorsResponse).Collaborators, res1
+}
+
+func (set EndpointsSet) ListSpaces(arg0 context.Context, arg1 string) (res0 []*collaborators.Collaborator, res1 error) {
+	request := ListSpacesRequest{Subject: arg1}
+	response, res1 := set.ListSpacesEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*ListSpacesResponse).Spaces, res1
+}
diff --git a/pkg/collaborators/transport/endpoints.microgen.go b/pkg/collaborators/transport/endpoints.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..022bf403e3a574918d006786c54cfc854f318915
--- /dev/null
+++ b/pkg/collaborators/transport/endpoints.microgen.go
@@ -0,0 +1,14 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import endpoint "github.com/go-kit/kit/endpoint"
+
+// EndpointsSet implements Collaborators API and used for transport purposes.
+type EndpointsSet struct {
+	SetEndpoint               endpoint.Endpoint
+	GetEndpoint               endpoint.Endpoint
+	RemoveEndpoint            endpoint.Endpoint
+	ListCollaboratorsEndpoint endpoint.Endpoint
+	ListSpacesEndpoint        endpoint.Endpoint
+}
diff --git a/pkg/collaborators/transport/exchanges.microgen.go b/pkg/collaborators/transport/exchanges.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..a51674f40f696f15d1f625fbf7923bdd4879e6b5
--- /dev/null
+++ b/pkg/collaborators/transport/exchanges.microgen.go
@@ -0,0 +1,44 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import collaborators "git.perx.ru/perxis/perxis-go/pkg/collaborators"
+
+type (
+	SetRequest struct {
+		SpaceId string `json:"space_id"`
+		Subject string `json:"subject"`
+		Role    string `json:"role"`
+	}
+	// Formal exchange type, please do not delete.
+	SetResponse struct{}
+
+	GetRequest struct {
+		SpaceId string `json:"space_id"`
+		Subject string `json:"subject"`
+	}
+	GetResponse struct {
+		Role string `json:"role"`
+	}
+
+	RemoveRequest struct {
+		SpaceId string `json:"space_id"`
+		Subject string `json:"subject"`
+	}
+	// Formal exchange type, please do not delete.
+	RemoveResponse struct{}
+
+	ListCollaboratorsRequest struct {
+		SpaceId string `json:"space_id"`
+	}
+	ListCollaboratorsResponse struct {
+		Collaborators []*collaborators.Collaborator `json:"collaborators"`
+	}
+
+	ListSpacesRequest struct {
+		Subject string `json:"subject"`
+	}
+	ListSpacesResponse struct {
+		Spaces []*collaborators.Collaborator `json:"spaces"`
+	}
+)
diff --git a/pkg/collaborators/transport/grpc/client.microgen.go b/pkg/collaborators/transport/grpc/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..eaf778f878438fc2a44ef980a25a3aed803fcee8
--- /dev/null
+++ b/pkg/collaborators/transport/grpc/client.microgen.go
@@ -0,0 +1,54 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/collaborators/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/collaborators"
+	grpckit "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	grpc "google.golang.org/grpc"
+)
+
+func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet {
+	if addr == "" {
+		addr = "content.collaborators.Collaborators"
+	}
+	return transport.EndpointsSet{
+		GetEndpoint: grpckit.NewClient(
+			conn, addr, "Get",
+			_Encode_Get_Request,
+			_Decode_Get_Response,
+			pb.GetResponse{},
+			opts...,
+		).Endpoint(),
+		ListCollaboratorsEndpoint: grpckit.NewClient(
+			conn, addr, "ListCollaborators",
+			_Encode_ListCollaborators_Request,
+			_Decode_ListCollaborators_Response,
+			pb.ListCollaboratorsResponse{},
+			opts...,
+		).Endpoint(),
+		ListSpacesEndpoint: grpckit.NewClient(
+			conn, addr, "ListSpaces",
+			_Encode_ListSpaces_Request,
+			_Decode_ListSpaces_Response,
+			pb.ListSpacesResponse{},
+			opts...,
+		).Endpoint(),
+		RemoveEndpoint: grpckit.NewClient(
+			conn, addr, "Remove",
+			_Encode_Remove_Request,
+			_Decode_Remove_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+		SetEndpoint: grpckit.NewClient(
+			conn, addr, "Set",
+			_Encode_Set_Request,
+			_Decode_Set_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+	}
+}
diff --git a/pkg/collaborators/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/collaborators/transport/grpc/protobuf_endpoint_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec484dfe3b9a8ee1cb38d1d1d03cbfc5ce7a1f8a
--- /dev/null
+++ b/pkg/collaborators/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -0,0 +1,193 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// Please, do not change functions names!
+package transportgrpc
+
+import (
+	"context"
+	"errors"
+
+	transport "git.perx.ru/perxis/perxis-go/pkg/collaborators/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/collaborators"
+	empty "github.com/golang/protobuf/ptypes/empty"
+)
+
+func _Encode_Set_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil SetRequest")
+	}
+	req := request.(*transport.SetRequest)
+	return &pb.SetRequest{
+		Role:    req.Role,
+		SpaceId: req.SpaceId,
+		Subject: req.Subject,
+	}, nil
+}
+
+func _Encode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetRequest")
+	}
+	req := request.(*transport.GetRequest)
+	return &pb.GetRequest{
+		SpaceId: req.SpaceId,
+		Subject: req.Subject,
+	}, nil
+}
+
+func _Encode_Remove_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil RemoveRequest")
+	}
+	req := request.(*transport.RemoveRequest)
+	return &pb.RemoveRequest{
+		SpaceId: req.SpaceId,
+		Subject: req.Subject,
+	}, nil
+}
+
+func _Encode_ListCollaborators_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListCollaboratorsRequest")
+	}
+	req := request.(*transport.ListCollaboratorsRequest)
+	return &pb.ListCollaboratorsRequest{SpaceId: req.SpaceId}, nil
+}
+
+func _Encode_ListSpaces_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListSpacesRequest")
+	}
+	req := request.(*transport.ListSpacesRequest)
+	return &pb.ListSpacesRequest{Subject: req.Subject}, nil
+}
+
+func _Encode_Set_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Encode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetResponse")
+	}
+	resp := response.(*transport.GetResponse)
+	return &pb.GetResponse{Role: resp.Role}, nil
+}
+
+func _Encode_Remove_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Encode_ListCollaborators_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListCollaboratorsResponse")
+	}
+	resp := response.(*transport.ListCollaboratorsResponse)
+	respCollaborators, err := ListPtrCollaboratorToProto(resp.Collaborators)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.ListCollaboratorsResponse{Collaborators: respCollaborators}, nil
+}
+
+func _Encode_ListSpaces_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListSpacesResponse")
+	}
+	resp := response.(*transport.ListSpacesResponse)
+	respSpaces, err := ListPtrCollaboratorToProto(resp.Spaces)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.ListSpacesResponse{Spaces: respSpaces}, nil
+}
+
+func _Decode_Set_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil SetRequest")
+	}
+	req := request.(*pb.SetRequest)
+	return &transport.SetRequest{
+		Role:    string(req.Role),
+		SpaceId: string(req.SpaceId),
+		Subject: string(req.Subject),
+	}, nil
+}
+
+func _Decode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetRequest")
+	}
+	req := request.(*pb.GetRequest)
+	return &transport.GetRequest{
+		SpaceId: string(req.SpaceId),
+		Subject: string(req.Subject),
+	}, nil
+}
+
+func _Decode_Remove_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil RemoveRequest")
+	}
+	req := request.(*pb.RemoveRequest)
+	return &transport.RemoveRequest{
+		SpaceId: string(req.SpaceId),
+		Subject: string(req.Subject),
+	}, nil
+}
+
+func _Decode_ListCollaborators_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListCollaboratorsRequest")
+	}
+	req := request.(*pb.ListCollaboratorsRequest)
+	return &transport.ListCollaboratorsRequest{SpaceId: string(req.SpaceId)}, nil
+}
+
+func _Decode_ListSpaces_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListSpacesRequest")
+	}
+	req := request.(*pb.ListSpacesRequest)
+	return &transport.ListSpacesRequest{Subject: string(req.Subject)}, nil
+}
+
+func _Decode_Set_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetResponse")
+	}
+	resp := response.(*pb.GetResponse)
+	return &transport.GetResponse{Role: string(resp.Role)}, nil
+}
+
+func _Decode_Remove_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_ListCollaborators_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListCollaboratorsResponse")
+	}
+	resp := response.(*pb.ListCollaboratorsResponse)
+	respCollaborators, err := ProtoToListPtrCollaborator(resp.Collaborators)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.ListCollaboratorsResponse{Collaborators: respCollaborators}, nil
+}
+
+func _Decode_ListSpaces_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListSpacesResponse")
+	}
+	resp := response.(*pb.ListSpacesResponse)
+	respSpaces, err := ProtoToListPtrCollaborator(resp.Spaces)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.ListSpacesResponse{Spaces: respSpaces}, nil
+}
diff --git a/pkg/collaborators/transport/grpc/protobuf_type_converters.microgen.go b/pkg/collaborators/transport/grpc/protobuf_type_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..5752acdf6235ab7b1bd09a46fc9ff3c8467ca7df
--- /dev/null
+++ b/pkg/collaborators/transport/grpc/protobuf_type_converters.microgen.go
@@ -0,0 +1,34 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// It is better for you if you do not change functions names!
+// This file will never be overwritten.
+package transportgrpc
+
+import (
+	service "git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	pbcommon "git.perx.ru/perxis/perxis-go/proto/common"
+)
+
+func ListPtrCollaboratorToProto(collaborators []*service.Collaborator) ([]*pbcommon.Collaborator, error) {
+	protoCollaborators := make([]*pbcommon.Collaborator, 0, len(collaborators))
+	for _, c := range collaborators {
+		protoCollaborators = append(protoCollaborators, &pbcommon.Collaborator{
+			SpaceId: c.SpaceID,
+			Subject: c.Subject,
+			Role:    c.Role,
+		})
+	}
+	return protoCollaborators, nil
+}
+
+func ProtoToListPtrCollaborator(protoCollaborators []*pbcommon.Collaborator) ([]*service.Collaborator, error) {
+	collaborators := make([]*service.Collaborator, 0, len(protoCollaborators))
+	for _, c := range protoCollaborators {
+		collaborators = append(collaborators, &service.Collaborator{
+			SpaceID: c.SpaceId,
+			Subject: c.Subject,
+			Role:    c.Role,
+		})
+	}
+	return collaborators, nil
+}
diff --git a/pkg/collaborators/transport/grpc/server.microgen.go b/pkg/collaborators/transport/grpc/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..6ece1b8b7d40f2c78d178ea88cf2aeb2ff7bb44f
--- /dev/null
+++ b/pkg/collaborators/transport/grpc/server.microgen.go
@@ -0,0 +1,97 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// DO NOT EDIT.
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/collaborators/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/collaborators"
+	grpc "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	context "golang.org/x/net/context"
+)
+
+type collaboratorsServer struct {
+	set               grpc.Handler
+	get               grpc.Handler
+	remove            grpc.Handler
+	listCollaborators grpc.Handler
+	listSpaces        grpc.Handler
+
+	pb.UnimplementedCollaboratorsServer
+}
+
+func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.CollaboratorsServer {
+	return &collaboratorsServer{
+		get: grpc.NewServer(
+			endpoints.GetEndpoint,
+			_Decode_Get_Request,
+			_Encode_Get_Response,
+			opts...,
+		),
+		listCollaborators: grpc.NewServer(
+			endpoints.ListCollaboratorsEndpoint,
+			_Decode_ListCollaborators_Request,
+			_Encode_ListCollaborators_Response,
+			opts...,
+		),
+		listSpaces: grpc.NewServer(
+			endpoints.ListSpacesEndpoint,
+			_Decode_ListSpaces_Request,
+			_Encode_ListSpaces_Response,
+			opts...,
+		),
+		remove: grpc.NewServer(
+			endpoints.RemoveEndpoint,
+			_Decode_Remove_Request,
+			_Encode_Remove_Response,
+			opts...,
+		),
+		set: grpc.NewServer(
+			endpoints.SetEndpoint,
+			_Decode_Set_Request,
+			_Encode_Set_Response,
+			opts...,
+		),
+	}
+}
+
+func (S *collaboratorsServer) Set(ctx context.Context, req *pb.SetRequest) (*empty.Empty, error) {
+	_, resp, err := S.set.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
+
+func (S *collaboratorsServer) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) {
+	_, resp, err := S.get.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.GetResponse), nil
+}
+
+func (S *collaboratorsServer) Remove(ctx context.Context, req *pb.RemoveRequest) (*empty.Empty, error) {
+	_, resp, err := S.remove.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
+
+func (S *collaboratorsServer) ListCollaborators(ctx context.Context, req *pb.ListCollaboratorsRequest) (*pb.ListCollaboratorsResponse, error) {
+	_, resp, err := S.listCollaborators.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.ListCollaboratorsResponse), nil
+}
+
+func (S *collaboratorsServer) ListSpaces(ctx context.Context, req *pb.ListSpacesRequest) (*pb.ListSpacesResponse, error) {
+	_, resp, err := S.listSpaces.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.ListSpacesResponse), nil
+}
diff --git a/pkg/collaborators/transport/server.microgen.go b/pkg/collaborators/transport/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..7d0a571d32ebe2ec2a49190a5d79136ff96ba8c1
--- /dev/null
+++ b/pkg/collaborators/transport/server.microgen.go
@@ -0,0 +1,60 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+
+	collaborators "git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	endpoint "github.com/go-kit/kit/endpoint"
+)
+
+func Endpoints(svc collaborators.Collaborators) EndpointsSet {
+	return EndpointsSet{
+		GetEndpoint:               GetEndpoint(svc),
+		ListCollaboratorsEndpoint: ListCollaboratorsEndpoint(svc),
+		ListSpacesEndpoint:        ListSpacesEndpoint(svc),
+		RemoveEndpoint:            RemoveEndpoint(svc),
+		SetEndpoint:               SetEndpoint(svc),
+	}
+}
+
+func SetEndpoint(svc collaborators.Collaborators) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*SetRequest)
+		res0 := svc.Set(arg0, req.SpaceId, req.Subject, req.Role)
+		return &SetResponse{}, res0
+	}
+}
+
+func GetEndpoint(svc collaborators.Collaborators) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetRequest)
+		res0, res1 := svc.Get(arg0, req.SpaceId, req.Subject)
+		return &GetResponse{Role: res0}, res1
+	}
+}
+
+func RemoveEndpoint(svc collaborators.Collaborators) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*RemoveRequest)
+		res0 := svc.Remove(arg0, req.SpaceId, req.Subject)
+		return &RemoveResponse{}, res0
+	}
+}
+
+func ListCollaboratorsEndpoint(svc collaborators.Collaborators) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*ListCollaboratorsRequest)
+		res0, res1 := svc.ListCollaborators(arg0, req.SpaceId)
+		return &ListCollaboratorsResponse{Collaborators: res0}, res1
+	}
+}
+
+func ListSpacesEndpoint(svc collaborators.Collaborators) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*ListSpacesRequest)
+		res0, res1 := svc.ListSpaces(arg0, req.Subject)
+		return &ListSpacesResponse{Spaces: res0}, res1
+	}
+}
diff --git a/pkg/data/crypto.go b/pkg/data/crypto.go
new file mode 100644
index 0000000000000000000000000000000000000000..6ec56170d27b43ae708a4277cdc2ae1d08d1a30d
--- /dev/null
+++ b/pkg/data/crypto.go
@@ -0,0 +1,63 @@
+package data
+
+import (
+	"crypto/rand"
+	"crypto/sha512"
+	"encoding/base64"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"golang.org/x/crypto/bcrypt"
+)
+
+func GetHash(value []byte) ([]byte, error) {
+	return bcrypt.GenerateFromPassword(value, 0)
+}
+
+func CompareHash(value, hash []byte) error {
+	return bcrypt.CompareHashAndPassword(hash, value)
+}
+
+func GetHashString(value string) (string, error) {
+	hash, err := GetHash([]byte(value))
+	if err != nil {
+		return "", err
+	}
+	return string(hash), nil
+}
+
+func CompareHashString(value, hash string) error {
+	return CompareHash([]byte(value), []byte(hash))
+}
+
+func GenerateRandomBytes(n int) []byte {
+	b := make([]byte, n)
+	_, err := rand.Read(b)
+	// Note that err == nil only if we read len(b) bytes.
+	if err != nil {
+		panic(errors.Wrap(err, "rand.Read error"))
+	}
+
+	return b
+}
+
+func GenerateRandomString(n int) string {
+	const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
+	bytes := GenerateRandomBytes(n)
+
+	for i, b := range bytes {
+		bytes[i] = letters[b%byte(len(letters))]
+	}
+	return string(bytes)
+}
+
+func GenerateRandomStringURLSafe(n int) string {
+	b := GenerateRandomBytes(n)
+	return base64.URLEncoding.EncodeToString(b)
+}
+
+func SignValue(signature, value string) string {
+	hash := sha512.Sum512([]byte(value + "_" + signature))
+	encoded := base64.StdEncoding.EncodeToString(hash[:])
+
+	return encoded
+}
diff --git a/pkg/data/list.go b/pkg/data/list.go
new file mode 100644
index 0000000000000000000000000000000000000000..e15a20cadcc2a22bc9c4797b8fa16413a511b5ab
--- /dev/null
+++ b/pkg/data/list.go
@@ -0,0 +1,186 @@
+package data
+
+import (
+	"fmt"
+
+	"gopkg.in/yaml.v3"
+)
+
+func Contains[T comparable](s T, arr []T) bool {
+
+	for _, elem := range arr {
+		if elem == s {
+			return true
+		}
+	}
+
+	return false
+}
+
+func ElementsMatch[T comparable](s1, s2 []T) bool {
+	if len(s1) != len(s2) {
+		return false
+	}
+	exists := make(map[T]bool)
+	for _, value := range s1 {
+		exists[value] = true
+	}
+	for _, value := range s2 {
+		if !exists[value] {
+			return false
+		}
+	}
+	return true
+}
+
+// Difference returns the elements in `a` that aren't in `b`.
+func Difference(a, b []string) []string {
+	mb := make(map[string]struct{}, len(b))
+	for _, x := range b {
+		mb[x] = struct{}{}
+	}
+	var diff []string
+	for _, x := range a {
+		if _, found := mb[x]; !found {
+			diff = append(diff, x)
+		}
+	}
+	return diff
+}
+
+// GetIntersection returns `a` elements that are in `b`.
+func GetIntersection[T comparable](a, b []T) []T {
+	mb := make(map[T]struct{}, len(b))
+	for _, x := range b {
+		mb[x] = struct{}{}
+	}
+	var match []T
+	for _, x := range a {
+		if _, found := mb[x]; found {
+			match = append(match, x)
+		}
+	}
+	return match
+}
+
+func GetXOR[T comparable](s1, s2 []T) []T {
+
+	res := make([]T, 0)
+
+	m1 := make(map[T]struct{}, len(s1))
+	m2 := make(map[T]struct{}, len(s2))
+
+	for _, s := range s1 {
+		m1[s] = struct{}{}
+	}
+
+	for _, s := range s2 {
+		if _, ok := m1[s]; !ok {
+			res = append(res, s)
+		}
+		m2[s] = struct{}{}
+	}
+
+	for _, s := range s1 {
+		if _, ok := m2[s]; !ok {
+			res = append(res, s)
+		}
+	}
+
+	return res
+}
+
+func SetFromSlice[T comparable](input []T) []T {
+	s := make([]T, 0, len(input))
+	m := make(map[T]bool)
+
+	for _, val := range input {
+		if _, ok := m[val]; !ok {
+			m[val] = true
+			s = append(s, val)
+		}
+	}
+
+	return s
+}
+
+func ToSliceOfInterfaces[T any](s []T) []interface{} {
+	if s == nil {
+		return nil
+	}
+	res := make([]interface{}, len(s))
+	for i, e := range s {
+		res[i] = e
+	}
+	return res
+}
+
+func ToSliceOfStrings(s []any) []string {
+	if s == nil {
+		return nil
+	}
+	res := make([]string, len(s))
+	for i, v := range s {
+		res[i] = fmt.Sprintf("%v", v)
+	}
+	return res
+}
+
+func MergeYaml(source, override string) (string, error) {
+	if source == "" {
+		return override, nil
+	}
+	if override == "" {
+		return source, nil
+	}
+
+	var srcYaml map[string]interface{}
+	err := yaml.Unmarshal([]byte(source), &srcYaml)
+	if err != nil {
+		return "", err
+	}
+
+	var overrideYaml map[string]interface{}
+	err = yaml.Unmarshal([]byte(override), &overrideYaml)
+	if err != nil {
+		return "", err
+	}
+
+	mergedMap := mergeMaps(srcYaml, overrideYaml)
+
+	mergedString, err := yaml.Marshal(mergedMap)
+	if err != nil {
+		return "", err
+	}
+
+	return string(mergedString), nil
+}
+
+func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
+	out := make(map[string]interface{}, len(a))
+	for k, v := range a {
+		out[k] = v
+	}
+	for k, v := range b {
+		if v, ok := v.(map[string]interface{}); ok {
+			if av, ok := out[k]; ok {
+				if av, ok := av.(map[string]interface{}); ok {
+					out[k] = mergeMaps(av, v)
+					continue
+				}
+			}
+		}
+
+		if v, ok := v.([]interface{}); ok {
+			if av, ok := out[k]; ok {
+				if av, ok := av.([]interface{}); ok {
+					out[k] = append(av, v...)
+					continue
+				}
+			}
+		}
+
+		out[k] = v
+	}
+	return out
+}
diff --git a/pkg/data/list_test.go b/pkg/data/list_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8231aae992e91f09f50bb71fb1e028e148c35993
--- /dev/null
+++ b/pkg/data/list_test.go
@@ -0,0 +1,141 @@
+package data
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestMergeYaml(t *testing.T) {
+	tests := []struct {
+		name     string
+		source   string
+		override string
+		res      string
+		wantErr  bool
+	}{
+		{"simple change",
+			"a: 1\nb: cat",
+			"a: 2",
+			"a: 2\nb: cat\n",
+			false,
+		},
+		{"simple not change",
+			"a: 1\nb: cat",
+			"a: 1",
+			"a: 1\nb: cat\n",
+			false,
+		},
+		{"object merge",
+			`
+keyA1: val1
+keyA2: val2
+keyB1: val1
+keyB2: val2`,
+			`
+keyС1: val1
+keyС2: val2
+keyÐ’1: val1
+keyÐ’2: val2`,
+			`keyA1: val1
+keyA2: val2
+keyB1: val1
+keyB2: val2
+keyÐ’1: val1
+keyÐ’2: val2
+keyС1: val1
+keyС2: val2
+`,
+			false,
+		},
+		{"object change",
+			`
+keyA1: val1
+keyA2: val2
+keyB1: val1
+keyB2: val2`,
+			`
+keyA1: val10
+keyA2: val20
+keyB1: val10
+keyB2: val20`,
+			`keyA1: val10
+keyA2: val20
+keyB1: val10
+keyB2: val20
+`,
+			false,
+		},
+		{"slice of objects merge",
+			`entries: 
+  - keyA1: val1
+    keyA2: val2
+  - keyB1: val1
+    keyB2: val2
+  - val3`,
+			`entries: 
+  - keyC1: val1
+    keyC2: val2
+  - keyD1: val1
+    keyD2: val2
+  - val30`,
+			`entries:
+    - keyA1: val1
+      keyA2: val2
+    - keyB1: val1
+      keyB2: val2
+    - val3
+    - keyC1: val1
+      keyC2: val2
+    - keyD1: val1
+      keyD2: val2
+    - val30
+`,
+			false,
+		},
+		{"slice of objects change",
+			`entries: 
+  - keyA1: val1
+    keyA2: val2
+  - keyB1: val1
+    keyB2: val2
+  - val3`,
+			`entries:
+  - keyA1: val10
+    keyA2: val20
+  - keyB1: val10
+    keyB2: val20`,
+			`entries:
+    - keyA1: val1
+      keyA2: val2
+    - keyB1: val1
+      keyB2: val2
+    - val3
+    - keyA1: val10
+      keyA2: val20
+    - keyB1: val10
+      keyB2: val20
+`,
+			false,
+		},
+		{"invalid string (missing space)",
+			"a:1",
+			"a:2",
+			"",
+			true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			res, err := MergeYaml(tt.source, tt.override)
+			assert.Equal(t, tt.res, res)
+			if tt.wantErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/data/strings.go b/pkg/data/strings.go
new file mode 100644
index 0000000000000000000000000000000000000000..36c3e6943526b17ed821476bc387382a4f72cb51
--- /dev/null
+++ b/pkg/data/strings.go
@@ -0,0 +1,91 @@
+package data
+
+import (
+	"regexp"
+	"strings"
+	"unicode"
+)
+
+var (
+	replacer = strings.NewReplacer("?", ".", "*", ".*")
+)
+
+func IsRegExp(s string) bool {
+	var prev rune
+	if len(s) < 2 {
+		return false
+	}
+
+	if s[0] == '^' || s[len(s)-1] == '$' {
+		return true
+	}
+
+	for _, r := range s {
+		switch r {
+		case '*', '+':
+			if prev == '.' {
+				return true
+			}
+		}
+		prev = r
+	}
+	return false
+}
+
+func CamelCase(name string) string {
+	newName := []rune{}
+	for i, c := range name {
+		if i == 0 {
+			newName = append(newName, unicode.ToLower(c))
+		} else {
+			newName = append(newName, c)
+		}
+	}
+	return string(newName)
+}
+
+func GlobMatch(s string, matches ...string) bool {
+
+	for _, match := range matches {
+		if match == "" {
+			return false
+		}
+
+		if s == match || match == "*" {
+			return true
+		}
+
+		ok, err := regexp.MatchString(GlobToRegexp(match), s)
+		if err != nil {
+			return false
+		}
+
+		if ok {
+			return true
+		}
+	}
+
+	return false
+}
+
+func GlobToRegexp(s string) string {
+	s = strings.TrimSpace(s)
+
+	if s == "*" {
+		return s
+	}
+	// если вначале не звездочка, то ищем с начала (важно для применения индексов mongo)
+	if s[0] != '*' {
+		s = "^" + s
+	} else {
+		s = s[1:]
+	}
+	// убираем последую звездочку, работает быстрее в mongo
+	if s[len(s)-1] == '*' {
+		s = s[:len(s)-1]
+	} else {
+		s = s + "$"
+	}
+
+	return replacer.Replace(s)
+}
diff --git a/pkg/data/strings_test.go b/pkg/data/strings_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a722cd4081ab8e4f04b3ef71369097024a98e320
--- /dev/null
+++ b/pkg/data/strings_test.go
@@ -0,0 +1,83 @@
+package data
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+func TestIsRegExp(t *testing.T) {
+	tests := []struct {
+		str  string
+		want bool
+	}{
+		{"^some", true},
+		{"some$", true},
+		{"some.*", true},
+		{"some.+", true},
+		{"some*", false},
+		{"some+", false},
+		{"so.me*", false},
+		{"some.", false},
+		{"s^om$e", false},
+	}
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("test%d", i), func(t *testing.T) {
+			if got := IsRegExp(tt.str); got != tt.want {
+				t.Errorf("IsRegExp(\"%s\") = %v, want %v", tt.str, got, tt.want)
+			}
+		})
+	}
+}
+
+func TestGlobToRegexp(t *testing.T) {
+	tests := []struct {
+		name string
+		args string
+		want interface{}
+	}{
+		{"from start", "aaa*", "^aaa"},
+		{"from end", "*aaa", "aaa$"},
+		{"middle", "*aaa*", "aaa"},
+		{"start end", "aa*bb", "^aa.*bb$"},
+		{"middle and chars", "aa*bb??", "^aa.*bb..$"},
+		{"anything", "*", "*"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := GlobToRegexp(tt.args); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Match() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestGlobMatch(t *testing.T) {
+	tests := []struct {
+		name string
+		s    string
+		glob []string
+		want bool
+	}{
+		{"from start", "aaateststring", []string{"aaa*"}, true},
+		{"from end", "teststringaaa", []string{"*aaa"}, true},
+		{"middle", "testaaastring", []string{"*aaa*"}, true},
+		{"start end", "aateststringbb", []string{"aa*bb"}, true},
+		{"any of", "teststringaa", []string{"aa*", "aa*bb", "*aa"}, true},
+		{"none matches", "teststringaa", []string{"aa*", "aa*bb", "*bb"}, false},
+		{"middle and chars", "aateststringbbcc", []string{"aa*bb??"}, true},
+		{"anything", "teststring", []string{"*"}, true},
+		{"false from start", "aateststring", []string{"aaa*"}, false},
+		{"false from end", "teststringaa", []string{"*aaa"}, false},
+		{"false middle", "testaastring", []string{"*aaa*"}, false},
+		{"false start end", "aateststringb", []string{"aa*bb"}, false},
+		{"false middle and chars", "ateststringbbcc", []string{"aa*bb??"}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := GlobMatch(tt.s, tt.glob...); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Match() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/errors/error.go b/pkg/errors/error.go
deleted file mode 100644
index cb8ec386db413ecc1a8f33f6dddd45c29359ea85..0000000000000000000000000000000000000000
--- a/pkg/errors/error.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package errors
-
-//import (
-//	"errors"
-//)
-
-//var (
-//	ErrSpaceRequired      = errors.New("space required")
-//	ErrSpaceHostRequired  = errors.New("space host required")
-//	ErrSpaceNotFound      = errors.New("space not found")
-//	ErrSpaceAlreadyExists = errors.New("space already exists")
-//)
-//
-//var errs = []error{
-//	ErrSpaceRequired,
-//	ErrSpaceHostRequired,
-//	ErrSpaceNotFound,
-//	ErrSpaceAlreadyExists,
-//}
-//
-//var errMap map[string]error
-//
-////func New(text string) error {
-////	if errMap == nil {
-////		errMap = make(map[string]error)
-////		for _, e := range errs {
-////			errMap[e.Error()] = e
-////		}
-////	}
-////
-////	if e, ok := errMap[text]; ok {
-////		return e
-////	}
-////
-////	return errors.New(text)
-////}
-//
-//func IsAny(err error, targets ...error) bool {
-//	for _, t := range targets {
-//		if errors.Is(err, t) {
-//			return true
-//		}
-//	}
-//	return false
-//}
-//
-//var (
-//	Is     = errors.Is
-//	As     = errors.As
-//	Unwrap = errors.Unwrap
-//)
diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go
index 952cd8688db8dffe737f63ccba6e1abd4521f715..1b3fe1b2198272d8c17c71fc28ae315b686a4ae5 100644
--- a/pkg/errors/errors.go
+++ b/pkg/errors/errors.go
@@ -14,59 +14,4 @@ var (
 	Errorf = errors.Errorf
 	Wrap   = errors.Wrap
 	Wrapf  = errors.Wrapf
-
-//Wrap          = errors.Wrap
-//Wrapf         = errors.Wrapf
-//WithMessage   = errors.WithMessage
-//CombineErrors = errors.CombineErrors
-////Detail    = errors.Detail
-////WithDetailf   = errors.WithDetailf
-////WithHint = errors.WithHint
-
-//Mark   = errors.Mark
-//Tags   = errors.WithContextTags
-//
-//CoreDomain      = errors.Domain("core")
-//StorageDomain   = errors.Domain("storage")
-//ServiceDomain   = errors.Domain("service")
-//TransportDomain = errors.Domain("transport")
-//EventsDomain    = errors.Domain("events")
 )
-
-//func New(msg string) error {
-//	return &Error{Message: msg}
-//}
-
-//func Trace(err error) error {
-//	return WithStack(WithID(err))
-//}
-//
-//func Wrap(err error, msg string) error {
-//	return WithID(errors.Wrap(err, msg))
-//}
-//
-//func ErrorBuilder(domain string) func(string) error {
-//	return func(msg string) error {
-//		return WithDomain(WithID(New(msg)), domain)
-//	}
-//}
-//
-//func ErrorfBuilder(domain string) func(format string, args ...interface{}) error {
-//	return func(format string, args ...interface{}) error {
-//		return WithDomain(WithID(Errorf(format, args...)), domain)
-//	}
-//}
-//
-//type Error struct {
-//	Message string
-//}
-//
-//func (e *Error) Error() string { return e.Message }
-//func (e *Error) Is(err error) bool {
-//	if errr, ok := err.(*Error); ok {
-//		if e.Message == errr.Message {
-//			return true
-//		}
-//	}
-//	return false
-//}
diff --git a/pkg/expr/bench.txt b/pkg/expr/bench.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0a8b9b81854561ae9a0ea3484a3fe8c9a544565b
--- /dev/null
+++ b/pkg/expr/bench.txt
@@ -0,0 +1,30 @@
+------
+BenchmarkConvertToMongo
+------
+Конвертация выражения expr `id in [ ...ids ]` в формат bson.
+
+test 1:
+
+Количество идентификаторов в фильтре: 10_000
+Размер выражения: 230007b (0,2Mb)
+
+goos: darwin
+goarch: amd64
+pkg: github.com/perxteam/perxis/pkg/expr
+cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
+BenchmarkConvertToMongo-12            27          44312238 ns/op        13374293 B/op      81067 allocs/op
+PASS
+ok      github.com/perxteam/perxis/pkg/expr     2.572s
+
+test 2:
+
+Количество идентификаторов в фильтре: 1_000_000
+Размер выражения: 23000007b (21,9Mb)
+
+goos: darwin
+goarch: amd64
+pkg: github.com/perxteam/perxis/pkg/expr
+cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
+BenchmarkConvertToMongo-12             1        4142071283 ns/op        1064427296 B/op  7135952 allocs/op
+PASS
+ok      github.com/perxteam/perxis/pkg/expr     4.646s
diff --git a/pkg/expr/config.go b/pkg/expr/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..628111173d92f81ccdcc5e4c40b1aecc2ff27626
--- /dev/null
+++ b/pkg/expr/config.go
@@ -0,0 +1,85 @@
+package expr
+
+import (
+	"github.com/antonmedv/expr"
+	"github.com/antonmedv/expr/conf"
+)
+
+type ExprConfig struct {
+	options []expr.Option
+}
+
+func (c *ExprConfig) RegisterOption(opt ...expr.Option) {
+	c.options = append(c.options, opt...)
+}
+
+func (c *ExprConfig) GetConfig(e map[string]interface{}) *conf.Config {
+	cfg := conf.New(e)
+
+	cfg.Operators = make(map[string][]string)
+	for _, opt := range c.options {
+		opt(cfg)
+	}
+
+	return cfg
+}
+
+var defaultConfig *ExprConfig
+
+func init() {
+	defaultConfig = &ExprConfig{}
+}
+
+func RegisterOption(opt ...expr.Option) {
+	defaultConfig.RegisterOption(opt...)
+}
+
+func GetDefaultConfig(e map[string]interface{}) *conf.Config {
+	return defaultConfig.GetConfig(e)
+}
+
+//func GetDefaultEnv(e map[string]interface{}) map[string]interface{} {
+//	return defaultConfig.GetEnv(e)
+//}
+
+func Extend(kv ...interface{}) expr.Option {
+	e := make(map[string]interface{})
+	i := 0
+	for {
+		if i+2 > len(kv) {
+			break
+		}
+		k, v := kv[i], kv[i+1]
+		name, kOk := k.(string)
+		if !kOk {
+			break
+		}
+
+		e[name] = v
+		i += 2
+	}
+	return ExtendMap(e)
+}
+
+func ExtendMap(e map[string]interface{}) expr.Option {
+	return func(c *conf.Config) {
+		var env map[string]interface{}
+		var ok bool
+		if c.Env == nil {
+			env = make(map[string]interface{})
+		} else {
+			if env, ok = c.Env.(map[string]interface{}); !ok {
+				panic("only map expr environment is supported")
+			}
+		}
+		for k, v := range e {
+			if _, ok := env[k]; !ok {
+				env[k] = v
+			}
+		}
+		c.Strict = true
+		c.MapEnv = true
+		c.Env = env
+		c.Types = conf.CreateTypesTable(c.Env)
+	}
+}
diff --git a/pkg/expr/context.go b/pkg/expr/context.go
new file mode 100644
index 0000000000000000000000000000000000000000..4049d96f5f62a94219b247b09a4337b474112224
--- /dev/null
+++ b/pkg/expr/context.go
@@ -0,0 +1,56 @@
+package expr
+
+import "context"
+
+type ctxKey struct{}
+
+type exprCtx struct {
+	Env map[string]interface{}
+}
+
+func getContext(ctx context.Context) (context.Context, *exprCtx) {
+	if ctx != nil {
+		if c, _ := ctx.Value(ctxKey{}).(*exprCtx); c != nil {
+			return ctx, c
+		}
+	} else {
+		ctx = context.Background()
+	}
+
+	c := &exprCtx{}
+	ctx = context.WithValue(ctx, ctxKey{}, c)
+	return ctx, c
+}
+
+func WithEnv(ctx context.Context, env map[string]interface{}) context.Context {
+	ctx, c := getContext(ctx)
+
+	if c.Env == nil {
+		c.Env = make(map[string]interface{})
+	}
+
+	for k, v := range env {
+		c.Env[k] = v
+	}
+
+	return ctx
+}
+
+func WithEnvKV(ctx context.Context, kv ...interface{}) context.Context {
+	m := make(map[string]interface{})
+
+	for i := 0; i < len(kv)/2; i++ {
+		key, ok := kv[i].(string)
+		if !ok {
+			return nil
+		}
+		m[key] = kv[i+1]
+	}
+
+	return WithEnv(ctx, m)
+}
+
+func GetEnv(ctx context.Context) map[string]interface{} {
+	ctx, c := getContext(ctx)
+	return c.Env
+}
diff --git a/pkg/expr/exp.go b/pkg/expr/exp.go
new file mode 100644
index 0000000000000000000000000000000000000000..e34b00b8e521dd9c915ee0bed9a6d62e1ac98aee
--- /dev/null
+++ b/pkg/expr/exp.go
@@ -0,0 +1,65 @@
+package expr
+
+import (
+	"fmt"
+	"strings"
+	"time"
+)
+
+func EqualString(f, s string) string {
+	return fmt.Sprintf("%s == '%s'", f, s)
+}
+
+func NotEqualString(f, s string) string {
+	return fmt.Sprintf("%s != '%s'", f, s)
+}
+
+func EqualNumber(f string, n int) string {
+	return fmt.Sprintf("%s == %d", f, n)
+}
+
+func NotEqualNumber(f string, n int) string {
+	return fmt.Sprintf("%s != %d", f, n)
+}
+
+func LessOrEqualNumber(f string, n int) string {
+	return fmt.Sprintf("%s <= %d", f, n)
+}
+
+func InStringArray(s string, arr []string) string {
+	arrStr := "'" + strings.Join(arr, "','") + "'"
+	return fmt.Sprintf("%s in [%s]", s, arrStr)
+}
+
+func NotInStringArray(s string, arr []string) string {
+	arrStr := "'" + strings.Join(arr, "','") + "'"
+	return fmt.Sprintf("%s not in [%s]", s, arrStr)
+}
+
+func And(ss ...string) string {
+	return strings.Join(ss, " && ")
+}
+
+func Or(ss ...string) string {
+	return strings.Join(ss, " || ")
+}
+
+func Not(s string) string {
+	return "!" + s
+}
+
+func After(f string, t time.Time) string {
+	return fmt.Sprintf("%s > Time.Time('%s')", f, t.Format(time.RFC3339))
+}
+
+func AfterOrEqual(f string, t time.Time) string {
+	return fmt.Sprintf("%s >= Time.Time('%s')", f, t.Format(time.RFC3339))
+}
+
+func Before(f string, t time.Time) string {
+	return fmt.Sprintf("%s < Time.Time('%s')", f, t.Format(time.RFC3339))
+}
+
+func BeforeOrEqual(f string, t time.Time) string {
+	return fmt.Sprintf("%s <= Time.Time('%s')", f, t.Format(time.RFC3339))
+}
diff --git a/pkg/expr/expr.go b/pkg/expr/expr.go
new file mode 100644
index 0000000000000000000000000000000000000000..9c788e71e108e85b483ec71a8700b7fc3bb046be
--- /dev/null
+++ b/pkg/expr/expr.go
@@ -0,0 +1,58 @@
+package expr
+
+import (
+	compiler2 "github.com/antonmedv/expr/compiler"
+	"github.com/antonmedv/expr/parser"
+	"github.com/antonmedv/expr/vm"
+	"golang.org/x/net/context"
+)
+
+const EnvContextKey = "$context"
+
+func Eval(ctx context.Context, input string, env map[string]interface{}) (interface{}, error) {
+	tree, err := parser.Parse(input)
+	if err != nil {
+		return nil, err
+	}
+
+	e := GetEnv(ctx)
+
+	if e == nil {
+		e = make(map[string]interface{})
+	}
+
+	for k, v := range env {
+		e[k] = v
+	}
+
+	e[EnvContextKey] = ctx
+	cfg := GetDefaultConfig(e)
+
+	env, _ = cfg.Env.(map[string]interface{})
+
+	program, err := compiler2.Compile(tree, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	output, err := vm.Run(program, env)
+	if err != nil {
+		return nil, err
+	}
+
+	return output, nil
+}
+
+func EvalKV(ctx context.Context, input string, kv ...interface{}) (interface{}, error) {
+	m := make(map[string]interface{})
+
+	for i := 0; i < len(kv)/2; i++ {
+		key, ok := kv[i].(string)
+		if !ok {
+			panic("key should be string")
+		}
+		m[key] = kv[i+1]
+	}
+
+	return Eval(ctx, input, m)
+}
diff --git a/pkg/expr/format.go b/pkg/expr/format.go
new file mode 100644
index 0000000000000000000000000000000000000000..730388d2d1e654549f8dae00332c6958efb26a65
--- /dev/null
+++ b/pkg/expr/format.go
@@ -0,0 +1,91 @@
+package expr
+
+import (
+	"fmt"
+	"path"
+	"regexp"
+	"strings"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/id"
+	"github.com/gosimple/slug"
+)
+
+var markersRE = regexp.MustCompile(`{\w+}`)
+
+func init() {
+	RegisterOption(
+		Extend(
+			"sprintf", Sprintf,
+			"trim_space", TrimSpace,
+			"to_upper", ToUpper,
+			"slugify", Slugify,
+			"make_path", MakePath,
+			"normalize_string", func(s string) string {
+				s = strings.TrimSpace(strings.ToLower(s))
+				s = strings.ReplaceAll(s, " ", "_")
+				s = strings.ReplaceAll(s, ".", "_")
+				s = strings.ReplaceAll(s, "/", "_")
+				return strings.ToValidUTF8(s, "")
+			},
+			"replace_markers", ReplaceMarkers,
+		),
+	)
+}
+
+func Slugify(value interface{}) string {
+	if s, ok := value.(string); ok {
+		return slug.Make(s)
+	}
+	return ""
+}
+
+func MakePath(base, url interface{}) string {
+	b, p := "/", ""
+	if s, ok := base.(string); ok && s != "" {
+		b = s
+	}
+
+	if s, ok := url.(string); ok {
+		p = s
+	}
+
+	return path.Join(b, p)
+}
+
+func ToUpper(value interface{}) string {
+	if s, ok := value.(string); ok {
+		return strings.ToUpper(s)
+	}
+	return ""
+}
+
+func TrimSpace(value interface{}) string {
+	if s, ok := value.(string); ok {
+		return strings.TrimSpace(s)
+	}
+	return ""
+}
+
+func Sprintf(format interface{}, a ...interface{}) string {
+	if f, ok := format.(string); ok {
+		return fmt.Sprintf(f, a...)
+	}
+	return ""
+}
+
+func ReplaceMarkers(value interface{}) string {
+	if s, ok := value.(string); ok {
+		s = markersRE.ReplaceAllStringFunc(s, func(m string) string {
+			switch {
+			case m == "{new_id}":
+				return id.GenerateNewID()
+			case m == "{now}":
+				return time.Now().Format(time.RFC3339)
+			}
+			return m
+		})
+		return s
+	}
+	return ""
+}
diff --git a/pkg/expr/format_test.go b/pkg/expr/format_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7a0ba4c67e899c058372ca65e050f58e83ec0073
--- /dev/null
+++ b/pkg/expr/format_test.go
@@ -0,0 +1,45 @@
+package expr
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/net/context"
+)
+
+func TestFormat(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		name  string
+		eval  string
+		env   map[string]interface{}
+		wantB interface{}
+	}{
+		{"sprintf#1", "sprintf(s1, s2)", map[string]interface{}{"s1": "hello %s", "s2": "world"}, "hello world"},
+		{"sprintf#2", "sprintf(s1, s2)", map[string]interface{}{"s1": nil, "s2": nil}, ""},
+		{"sprintf#3", "sprintf(s1, s2)", map[string]interface{}{"s1": "hello %s", "s2": nil}, "hello %!s(<nil>)"},
+		{"to_upper#1", "to_upper(s1)", map[string]interface{}{"s1": "hello"}, "HELLO"},
+		{"to_upper#2", "to_upper(s1)", map[string]interface{}{"s1": nil}, ""},
+		{"trim_space#1", "trim_space(s1)", map[string]interface{}{"s1": " hel lo  wor ld  "}, "hel lo  wor ld"},
+		{"trim_space#2", "trim_space(s1)", map[string]interface{}{"s1": nil}, ""},
+		{"slugify#1", "slugify(s1)", map[string]interface{}{"s1": " hello  world "}, "hello-world"},
+		{"slugify#2", "slugify(s1)", map[string]interface{}{"s1": " привет  мир "}, "privet-mir"},
+		{"slugify#3", "slugify(s1)", map[string]interface{}{"s1": "privet-mir"}, "privet-mir"},
+		{"slugify#4", "slugify(s1)", nil, ""},
+		{"make_path#1", "make_path(s1, s2)", map[string]interface{}{"s1": "/pages", "s2": "page-1"}, "/pages/page-1"},
+		{"make_path#2", "make_path(s1, s2)", map[string]interface{}{"s1": "/pages", "s2": ""}, "/pages"},
+		{"make_path#3", "make_path(s1, s2)", map[string]interface{}{"s1": "", "s2": "pages"}, "/pages"},
+		{"make_path#4", "make_path(s1, s2)", map[string]interface{}{"s1": "", "s2": ""}, "/"},
+		{"make_path#4", "make_path(s1, s2)", nil, "/"},
+		{"make_path#5", "make_path(s1, slugify(s2))", map[string]interface{}{"s1": "/pages", "s2": " привет  мир "}, "/pages/privet-mir"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotB, err := Eval(ctx, tt.eval, tt.env)
+			require.NoError(t, err)
+			assert.Equal(t, tt.wantB, gotB)
+		})
+	}
+}
diff --git a/pkg/expr/mongo.go b/pkg/expr/mongo.go
new file mode 100644
index 0000000000000000000000000000000000000000..70bd6f05e6f3e5c422c88e193a7e1621eaa2f9e6
--- /dev/null
+++ b/pkg/expr/mongo.go
@@ -0,0 +1,665 @@
+package expr
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+
+	"github.com/antonmedv/expr"
+	"github.com/antonmedv/expr/ast"
+	compiler2 "github.com/antonmedv/expr/compiler"
+	"github.com/antonmedv/expr/conf"
+	"github.com/antonmedv/expr/parser"
+	"go.mongodb.org/mongo-driver/bson"
+)
+
+var geoTypes = map[string]string{
+	"box":     "$box",
+	"polygon": "$polygon",
+}
+
+func ConvertToMongo(ctx context.Context, exp string, env map[string]interface{}, identifierRenameFn func(string) string, ops ...expr.Option) (b bson.M, err error) {
+	if exp == "" {
+		return bson.M{}, nil
+	}
+	tree, err := parser.Parse(exp)
+	if err != nil {
+		return nil, err
+	}
+	return convertToMongo(ctx, tree, env, identifierRenameFn, ops...)
+}
+
+func convertToMongo(ctx context.Context, tree *parser.Tree, env map[string]interface{}, identifierRenameFn func(string) string, ops ...expr.Option) (b bson.M, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	if env == nil {
+		env = make(map[string]interface{})
+	}
+
+	env[EnvContextKey] = ctx
+	config := GetDefaultConfig(env)
+
+	for _, op := range ops {
+		op(config)
+	}
+
+	env = config.Env.(map[string]interface{})
+
+	if len(config.Visitors) >= 0 {
+		for _, v := range config.Visitors {
+			ast.Walk(&tree.Node, v)
+		}
+	}
+
+	c := &compiler{tree: tree, env: env, config: config, identifierRenameFn: identifierRenameFn}
+	v := c.compile(tree.Node)
+	switch e := v.(type) {
+	case bson.M:
+		b = e
+	case string:
+		b = bson.M{"$text": bson.M{"$search": e}}
+	default:
+		err = fmt.Errorf("invalid expression")
+	}
+	return
+}
+
+type compiler struct {
+	env                map[string]interface{}
+	tree               *parser.Tree
+	config             *conf.Config
+	identifierRenameFn func(string) string
+}
+
+func (c *compiler) eval(node ast.Node) interface{} {
+	t := &parser.Tree{
+		Node:   node,
+		Source: c.tree.Source,
+	}
+	prg, err := compiler2.Compile(t, c.config)
+	if err != nil {
+		panic(fmt.Sprintf("compile error %s", err.Error()))
+	}
+	ret, err := expr.Run(prg, c.env)
+	if err != nil {
+		panic(fmt.Sprintf("execution error %s", err.Error()))
+	}
+	return ret
+}
+
+func (c *compiler) compile(node ast.Node) interface{} {
+	switch n := node.(type) {
+	case *ast.NilNode:
+		return c.NilNode(n)
+	case *ast.IdentifierNode:
+		return c.IdentifierNode(n)
+	case *ast.IntegerNode:
+		return c.IntegerNode(n)
+	case *ast.FloatNode:
+		return c.FloatNode(n)
+	case *ast.BoolNode:
+		return c.BoolNode(n)
+	case *ast.StringNode:
+		return c.StringNode(n)
+	case *ast.ConstantNode:
+		return c.ConstantNode(n)
+	case *ast.UnaryNode:
+		return c.UnaryNode(n)
+	case *ast.BinaryNode:
+		return c.BinaryNode(n)
+	case *ast.MatchesNode:
+		return c.MatchesNode(n)
+	case *ast.PropertyNode:
+		return c.PropertyNode(n)
+	case *ast.IndexNode:
+		return c.IndexNode(n)
+	case *ast.SliceNode:
+		return c.SliceNode(n)
+	case *ast.MethodNode:
+		return c.MethodNode(n)
+	case *ast.FunctionNode:
+		return c.FunctionNode(n)
+	case *ast.BuiltinNode:
+		return c.BuiltinNode(n)
+	case *ast.ClosureNode:
+		return c.ClosureNode(n)
+	case *ast.PointerNode:
+		return c.PointerNode(n)
+	case *ast.ConditionalNode:
+		return c.ConditionalNode(n)
+	case *ast.ArrayNode:
+		return c.ArrayNode(n)
+	case *ast.MapNode:
+		return c.MapNode(n)
+	case *ast.PairNode:
+		return c.PairNode(n)
+	default:
+		panic(fmt.Sprintf("undefined node type (%T)", node))
+	}
+}
+
+func (c *compiler) NilNode(node *ast.NilNode) interface{} {
+	return nil
+}
+
+func (c *compiler) IdentifierNode(node *ast.IdentifierNode) string {
+	identifier := node.Value
+	if c.identifierRenameFn != nil {
+		identifier = c.identifierRenameFn(identifier)
+	}
+	return identifier
+}
+
+func (c *compiler) IntegerNode(node *ast.IntegerNode) int {
+	return node.Value
+	//t := node.Type()
+	//if t == nil {
+	//	c.emitPush(node.Value)
+	//	return
+	//}
+	//
+	//switch t.Kind() {
+	//case reflect.Float32:
+	//	c.emitPush(float32(node.Value))
+	//case reflect.Float64:
+	//	c.emitPush(float64(node.Value))
+	//
+	//case reflect.Int:
+	//	c.emitPush(int(node.Value))
+	//case reflect.Int8:
+	//	c.emitPush(int8(node.Value))
+	//case reflect.Int16:
+	//	c.emitPush(int16(node.Value))
+	//case reflect.Int32:
+	//	c.emitPush(int32(node.Value))
+	//case reflect.Int64:
+	//	c.emitPush(int64(node.Value))
+	//
+	//case reflect.Uint:
+	//	c.emitPush(uint(node.Value))
+	//case reflect.Uint8:
+	//	c.emitPush(uint8(node.Value))
+	//case reflect.Uint16:
+	//	c.emitPush(uint16(node.Value))
+	//case reflect.Uint32:
+	//	c.emitPush(uint32(node.Value))
+	//case reflect.Uint64:
+	//	c.emitPush(uint64(node.Value))
+	//
+	//default:
+	//	c.emitPush(node.Value)
+	//}
+}
+
+func (c *compiler) FloatNode(node *ast.FloatNode) float64 {
+	return node.Value
+}
+
+func (c *compiler) BoolNode(node *ast.BoolNode) bool {
+	return node.Value
+}
+
+func (c *compiler) StringNode(node *ast.StringNode) string {
+	return node.Value
+}
+
+func (c *compiler) ConstantNode(node *ast.ConstantNode) interface{} {
+	return node.Value
+}
+
+func (c *compiler) UnaryNode(node *ast.UnaryNode) interface{} {
+	op := c.compile(node.Node)
+
+	switch node.Operator {
+
+	case "!", "not":
+		return bson.M{"$not": op}
+	default:
+		panic(fmt.Sprintf("unknown operator (%v)", node.Operator))
+	}
+}
+
+func (c *compiler) identifier(node ast.Node) string {
+	switch l := node.(type) {
+	case *ast.PropertyNode:
+		return c.PropertyNode(l)
+	case *ast.IdentifierNode:
+		return c.IdentifierNode(l)
+	}
+	panic(fmt.Sprintf("incorrect identifier node (%v) ", ast.Dump(node)))
+}
+
+func (c *compiler) BinaryNode(node *ast.BinaryNode) interface{} {
+	switch node.Operator {
+	case "==":
+		return bson.M{c.identifier(node.Left): c.eval(node.Right)}
+
+	case "!=":
+		return bson.M{c.identifier(node.Left): bson.M{"$ne": c.eval(node.Right)}}
+
+	case "or", "||":
+		return bson.M{"$or": bson.A{c.compile(node.Left), c.compile(node.Right)}}
+
+	case "and", "&&":
+		return bson.M{"$and": bson.A{c.compile(node.Left), c.compile(node.Right)}}
+
+	case "in":
+		return bson.M{c.identifier(node.Left): bson.M{"$in": c.eval(node.Right)}}
+
+	case "not in":
+		return bson.M{c.identifier(node.Left): bson.M{"$nin": c.eval(node.Right)}}
+
+	case "<":
+		return bson.M{c.identifier(node.Left): bson.M{"$lt": c.eval(node.Right)}}
+
+	case ">":
+		return bson.M{c.identifier(node.Left): bson.M{"$gt": c.eval(node.Right)}}
+
+	case "<=":
+		return bson.M{c.identifier(node.Left): bson.M{"$lte": c.eval(node.Right)}}
+
+	case ">=":
+		return bson.M{c.identifier(node.Left): bson.M{"$gte": c.eval(node.Right)}}
+
+	//case "+":
+	//	c.compile(node.Left)
+	//	c.compile(node.Right)
+	//	c.emit(OpAdd)
+	//
+	//case "-":
+	//	c.compile(node.Left)
+	//	c.compile(node.Right)
+	//	c.emit(OpSubtract)
+	//
+	//case "*":
+	//	c.compile(node.Left)
+	//	c.compile(node.Right)
+	//	c.emit(OpMultiply)
+	//
+	//case "/":
+	//	c.compile(node.Left)
+	//	c.compile(node.Right)
+	//	c.emit(OpDivide)
+	//
+	//case "%":
+	//	c.compile(node.Left)
+	//	c.compile(node.Right)
+	//	c.emit(OpModulo)
+	//
+	//case "**":
+	//	c.compile(node.Left)
+	//	c.compile(node.Right)
+	//	c.emit(OpExponent)
+
+	case "contains":
+		value, ok := c.eval(node.Right).(string)
+		if !ok {
+			panic("contains requires string as an argument")
+		}
+
+		return bson.M{c.identifier(node.Left): bson.M{"$regex": regexp.QuoteMeta(value)}}
+
+	case "startsWith":
+		value, ok := c.eval(node.Right).(string)
+		if !ok {
+			panic("startsWith requires string as an argument")
+		}
+
+		return bson.M{c.identifier(node.Left): bson.M{"$regex": fmt.Sprintf("^%s.*", regexp.QuoteMeta(value))}}
+
+	case "endsWith":
+		value, ok := c.eval(node.Right).(string)
+		if !ok {
+			panic("endsWith requires string as an argument")
+		}
+
+		return bson.M{c.identifier(node.Left): bson.M{"$regex": fmt.Sprintf(".*%s$", regexp.QuoteMeta(value))}}
+
+	case "..":
+		panic("unsupported range")
+
+	default:
+		panic(fmt.Sprintf("unknown operator (%v)", node.Operator))
+
+	}
+}
+
+func (c *compiler) MatchesNode(node *ast.MatchesNode) interface{} {
+	panic("unsupported match node")
+	//if node.Regexp != nil {
+	//	c.compile(node.Left)
+	//	c.emit(OpMatchesConst, c.makeConstant(node.Regexp)...)
+	//	return
+	//}
+	//c.compile(node.Left)
+	//c.compile(node.Right)
+	//c.emit(OpMatches)
+}
+
+func (c *compiler) PropertyNode(node *ast.PropertyNode) string {
+	v := c.compile(node.Node)
+	if val, ok := v.(string); ok {
+		return fmt.Sprintf("%s.%s", val, node.Property)
+	}
+	panic(fmt.Sprintf("unsupported property for %v", ast.Dump(node.Node)))
+}
+
+func (c *compiler) IndexNode(node *ast.IndexNode) string {
+	return fmt.Sprintf("{index-%v}", c.compile(node.Index))
+}
+
+func (c *compiler) SliceNode(node *ast.SliceNode) interface{} {
+	panic("unsupported slice node")
+}
+
+func (c *compiler) MethodNode(node *ast.MethodNode) interface{} {
+	panic("unsupported method node")
+	//c.compile(node.Node)
+	//for _, arg := range node.Arguments {
+	//	c.compile(arg)
+	//}
+	//c.emit(OpMethod, c.makeConstant(Call{Name: node.Method, Size: len(node.Arguments)})...)
+}
+
+func (c *compiler) FunctionNode(node *ast.FunctionNode) interface{} {
+	switch node.Name {
+	case "search", "q":
+		val := c.compile(node.Arguments[0])
+		return bson.M{"$text": bson.M{"$search": val}}
+	case "near":
+		v := c.identifier(node.Arguments[0])
+		point := c.eval(node.Arguments[1])
+		distance := c.eval(node.Arguments[2])
+
+		if v == "" {
+			panic("incorrect argument, empty field name")
+		}
+		if !strings.HasSuffix(v, ".geometry") {
+			v += ".geometry"
+		}
+
+		if _, ok := point.([]interface{}); !ok {
+			panic("incorrect argument, point must coordinates array")
+		}
+
+		return bson.M{
+			v: bson.M{"$near": bson.D{{Key: "$geometry", Value: map[string]interface{}{"type": "Point", "coordinates": point}}, {Key: "$maxDistance", Value: distance}}},
+		}
+	case "within":
+		v := c.identifier(node.Arguments[0])
+		t := c.eval(node.Arguments[1])
+		points := c.eval(node.Arguments[2])
+
+		if v == "" {
+			panic("incorrect argument, empty field name")
+		}
+
+		if !strings.HasSuffix(v, ".geometry") {
+			v += ".geometry"
+		}
+
+		typ, ok := t.(string)
+		if !ok {
+			panic("incorrect argument, geotype must be string")
+		}
+		typ, ok = geoTypes[typ]
+		if !ok {
+			panic("incorrect geotype value")
+		}
+
+		if _, ok := points.([]interface{}); !ok {
+			panic("incorrect argument, points must be array of coordinates")
+		}
+
+		return bson.M{
+			v: bson.M{"$geoWithin": bson.M{typ: points}},
+		}
+	case "In":
+		fields := c.identifier(node.Arguments[0])
+		if fields == "" {
+			panic("incorrect argument, empty field name")
+		}
+		array, ok := c.eval(node.Arguments[1]).([]interface{})
+		if !ok {
+			array = []interface{}{c.eval(node.Arguments[1])}
+		}
+
+		return bson.M{fields: bson.M{"$in": array}}
+
+	case "icontains":
+		v := c.identifier(node.Arguments[0])
+		t, ok := c.eval(node.Arguments[1]).(string)
+		if !ok {
+			panic("icontains requires string as an argument")
+		}
+		return bson.M{v: bson.M{"$regex": regexp.QuoteMeta(t), "$options": "i"}}
+
+	case "istartsWith":
+		v := c.identifier(node.Arguments[0])
+		t, ok := c.eval(node.Arguments[1]).(string)
+		if !ok {
+			panic("istartsWith requires string as an argument")
+		}
+		return bson.M{v: bson.M{"$regex": fmt.Sprintf("^%s.*", regexp.QuoteMeta(t)), "$options": "i"}}
+
+	case "iendsWith":
+		v := c.identifier(node.Arguments[0])
+		t, ok := c.eval(node.Arguments[1]).(string)
+		if !ok {
+			panic("iendsWith requires string as an argument")
+		}
+		return bson.M{v: bson.M{"$regex": fmt.Sprintf(".*%s$", regexp.QuoteMeta(t)), "$options": "i"}}
+	}
+	panic("unsupported function")
+	//for _, arg := range node.Arguments {
+	//	c.compile(arg)
+	//}
+	//op := OpCall
+	//if node.Fast {
+	//	op = OpCallFast
+	//}
+	//c.emit(op, c.makeConstant(Call{Name: node.Name, Size: len(node.Arguments)})...)
+}
+
+func (c *compiler) BuiltinNode(node *ast.BuiltinNode) interface{} {
+	panic("unsupported builin node")
+	//switch node.Name {
+	//case "len":
+	//	c.compile(node.Arguments[0])
+	//	c.emit(OpLen)
+	//	c.emit(OpRot)
+	//	c.emit(OpPop)
+	//
+	//case "all":
+	//	c.compile(node.Arguments[0])
+	//	c.emit(OpBegin)
+	//	var loopBreak int
+	//	c.emitLoop(func() {
+	//		c.compile(node.Arguments[1])
+	//		loopBreak = c.emit(OpJumpIfFalse, c.placeholder()...)
+	//		c.emit(OpPop)
+	//	})
+	//	c.emit(OpTrue)
+	//	c.patchJump(loopBreak)
+	//	c.emit(OpEnd)
+	//
+	//case "none":
+	//	c.compile(node.Arguments[0])
+	//	c.emit(OpBegin)
+	//	var loopBreak int
+	//	c.emitLoop(func() {
+	//		c.compile(node.Arguments[1])
+	//		c.emit(OpNot)
+	//		loopBreak = c.emit(OpJumpIfFalse, c.placeholder()...)
+	//		c.emit(OpPop)
+	//	})
+	//	c.emit(OpTrue)
+	//	c.patchJump(loopBreak)
+	//	c.emit(OpEnd)
+	//
+	//case "any":
+	//	c.compile(node.Arguments[0])
+	//	c.emit(OpBegin)
+	//	var loopBreak int
+	//	c.emitLoop(func() {
+	//		c.compile(node.Arguments[1])
+	//		loopBreak = c.emit(OpJumpIfTrue, c.placeholder()...)
+	//		c.emit(OpPop)
+	//	})
+	//	c.emit(OpFalse)
+	//	c.patchJump(loopBreak)
+	//	c.emit(OpEnd)
+	//
+	//case "one":
+	//	count := c.makeConstant("count")
+	//	c.compile(node.Arguments[0])
+	//	c.emit(OpBegin)
+	//	c.emitPush(0)
+	//	c.emit(OpStore, count...)
+	//	c.emitLoop(func() {
+	//		c.compile(node.Arguments[1])
+	//		c.emitCond(func() {
+	//			c.emit(OpInc, count...)
+	//		})
+	//	})
+	//	c.emit(OpLoad, count...)
+	//	c.emitPush(1)
+	//	c.emit(OpEqual)
+	//	c.emit(OpEnd)
+	//
+	//case "filter":
+	//	count := c.makeConstant("count")
+	//	c.compile(node.Arguments[0])
+	//	c.emit(OpBegin)
+	//	c.emitPush(0)
+	//	c.emit(OpStore, count...)
+	//	c.emitLoop(func() {
+	//		c.compile(node.Arguments[1])
+	//		c.emitCond(func() {
+	//			c.emit(OpInc, count...)
+	//
+	//			c.emit(OpLoad, c.makeConstant("array")...)
+	//			c.emit(OpLoad, c.makeConstant("i")...)
+	//			c.emit(OpIndex)
+	//		})
+	//	})
+	//	c.emit(OpLoad, count...)
+	//	c.emit(OpEnd)
+	//	c.emit(OpArray)
+	//
+	//case "map":
+	//	c.compile(node.Arguments[0])
+	//	c.emit(OpBegin)
+	//	size := c.emitLoop(func() {
+	//		c.compile(node.Arguments[1])
+	//	})
+	//	c.emit(OpLoad, size...)
+	//	c.emit(OpEnd)
+	//	c.emit(OpArray)
+	//
+	//case "count":
+	//	count := c.makeConstant("count")
+	//	c.compile(node.Arguments[0])
+	//	c.emit(OpBegin)
+	//	c.emitPush(0)
+	//	c.emit(OpStore, count...)
+	//	c.emitLoop(func() {
+	//		c.compile(node.Arguments[1])
+	//		c.emitCond(func() {
+	//			c.emit(OpInc, count...)
+	//		})
+	//	})
+	//	c.emit(OpLoad, count...)
+	//	c.emit(OpEnd)
+	//
+	//default:
+	//	panic(fmt.Sprintf("unknown builtin %v", node.Name))
+	//}
+}
+
+//func (c *compiler) emitLoop(body func()) []byte {
+//	i := c.makeConstant("i")
+//	size := c.makeConstant("size")
+//	array := c.makeConstant("array")
+//
+//	c.emit(OpLen)
+//	c.emit(OpStore, size...)
+//	c.emit(OpStore, array...)
+//	c.emitPush(0)
+//	c.emit(OpStore, i...)
+//
+//	cond := len(c.bytecode)
+//	c.emit(OpLoad, i...)
+//	c.emit(OpLoad, size...)
+//	c.emit(OpLess)
+//	end := c.emit(OpJumpIfFalse, c.placeholder()...)
+//	c.emit(OpPop)
+//
+//	body()
+//
+//	c.emit(OpInc, i...)
+//	c.emit(OpJumpBackward, c.calcBackwardJump(cond)...)
+//
+//	c.patchJump(end)
+//	c.emit(OpPop)
+//
+//	return size
+//}
+
+func (c *compiler) ClosureNode(node *ast.ClosureNode) interface{} {
+	return c.compile(node.Node)
+}
+
+func (c *compiler) PointerNode(node *ast.PointerNode) interface{} {
+	panic("unsupported pointer node")
+	//c.emit(OpLoad, c.makeConstant("array")...)
+	//c.emit(OpLoad, c.makeConstant("i")...)
+	//c.emit(OpIndex)
+}
+
+func (c *compiler) ConditionalNode(node *ast.ConditionalNode) interface{} {
+	panic("unsupported conditional node")
+	//c.compile(node.Cond)
+	//otherwise := c.emit(OpJumpIfFalse, c.placeholder()...)
+	//
+	//c.emit(OpPop)
+	//c.compile(node.Exp1)
+	//end := c.emit(OpJump, c.placeholder()...)
+	//
+	//c.patchJump(otherwise)
+	//c.emit(OpPop)
+	//c.compile(node.Exp2)
+	//
+	//c.patchJump(end)
+}
+
+func (c *compiler) ArrayNode(node *ast.ArrayNode) interface{} {
+	panic("unsupported array node")
+	//for _, node := range node.Nodes {
+	//	c.compile(node)
+	//}
+	//
+	//c.emitPush(len(node.Nodes))
+	//c.emit(OpArray)
+}
+
+func (c *compiler) MapNode(node *ast.MapNode) interface{} {
+	panic("unsupported map node")
+	//for _, pair := range node.Pairs {
+	//	c.compile(pair)
+	//}
+	//
+	//c.emitPush(len(node.Pairs))
+	//c.emit(OpMap)
+}
+
+func (c *compiler) PairNode(node *ast.PairNode) interface{} {
+	panic("unsupported pair node")
+	//c.compile(node.Key)
+	//c.compile(node.Value)
+}
diff --git a/pkg/expr/mongo_test.go b/pkg/expr/mongo_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..46f92b551086ae7ad1d5336f35c98d3154b4253d
--- /dev/null
+++ b/pkg/expr/mongo_test.go
@@ -0,0 +1,86 @@
+package expr
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/id"
+	"github.com/antonmedv/expr"
+	"github.com/antonmedv/expr/ast"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.mongodb.org/mongo-driver/bson"
+	"golang.org/x/net/context"
+)
+
+func TestConvertToMongo(t *testing.T) {
+	now := time.Now()
+	dt, _ := time.Parse("2006-01-02", "2021-08-31")
+	tm, _ := time.Parse(time.RFC3339, now.Format(time.RFC3339))
+	ctx := context.Background()
+
+	tests := []struct {
+		name    string
+		eval    string
+		env     map[string]interface{}
+		wantB   bson.M
+		wantErr bool
+	}{
+		{"equal", "s == 3", nil, bson.M{"s": 3}, false},
+		{"in array", "s in [1,2,3]", nil, bson.M{"s": bson.M{"$in": []interface{}{1, 2, 3}}}, false},
+		{"contains", "s contains 'some'", nil, bson.M{"s": bson.M{"$regex": "some"}}, false},
+		{"contains with . + () $ {} ^", "value contains 'something with . + () $ {} ^'", nil, bson.M{"value": bson.M{"$regex": "something with \\. \\+ \\(\\) \\$ \\{\\} \\^"}}, false},
+		{"startsWith", "s startsWith 'some'", nil, bson.M{"s": bson.M{"$regex": "^some.*"}}, false},
+		{"startsWith . + () $ {} ^", "s startsWith '. + () $ {} ^'", nil, bson.M{"s": bson.M{"$regex": "^\\. \\+ \\(\\) \\$ \\{\\} \\^.*"}}, false},
+		{"endsWith", "s endsWith 'some'", nil, bson.M{"s": bson.M{"$regex": ".*some$"}}, false},
+		{"endsWith . + () $ {} ^", "s endsWith '. + () $ {} ^'", nil, bson.M{"s": bson.M{"$regex": ".*\\. \\+ \\(\\) \\$ \\{\\} \\^$"}}, false},
+		{"icontains", "icontains(s, 'some')", nil, bson.M{"s": bson.M{"$regex": "some", "$options": "i"}}, false},
+		{"icontains with . + () $ {} ^", "icontains (value, 'something with . + () $ {} ^')", nil, bson.M{"value": bson.M{"$regex": "something with \\. \\+ \\(\\) \\$ \\{\\} \\^", "$options": "i"}}, false},
+		{"istartsWith", "istartsWith(s, 'Some')", nil, bson.M{"s": bson.M{"$regex": "^Some.*", "$options": "i"}}, false},
+		{"istartsWith . + () $ {} ^ . + () $ {} ^", "istartsWith(s, '. + () $ {} ^')", nil, bson.M{"s": bson.M{"$regex": "^\\. \\+ \\(\\) \\$ \\{\\} \\^.*", "$options": "i"}}, false},
+		{"iendsWith", "iendsWith(s, 'some')", nil, bson.M{"s": bson.M{"$regex": ".*some$", "$options": "i"}}, false},
+		{"iendsWith . + () $ {} ^", "iendsWith(s,'. + () $ {} ^')", nil, bson.M{"s": bson.M{"$regex": ".*\\. \\+ \\(\\) \\$ \\{\\} \\^$", "$options": "i"}}, false},
+		{"or", "s==2 || s > 10", nil, bson.M{"$or": bson.A{bson.M{"s": 2}, bson.M{"s": bson.M{"$gt": 10}}}}, false},
+		{"search", "search('some') || s > 10", nil, bson.M{"$or": bson.A{bson.M{"$text": bson.M{"$search": "some"}}, bson.M{"s": bson.M{"$gt": 10}}}}, false},
+		{"vars:or", "s== a + 2 || s > a + 10", map[string]interface{}{"a": 100}, bson.M{"$or": bson.A{bson.M{"s": 102}, bson.M{"s": bson.M{"$gt": 110}}}}, false},
+		{"near", "near(a, [55.5, 37.5], 1000)", map[string]interface{}{"a": []interface{}{55, 37}}, bson.M{"a.geometry": bson.M{"$near": bson.D{{Key: "$geometry", Value: map[string]interface{}{"coordinates": []interface{}{55.5, 37.5}, "type": "Point"}}, {Key: "$maxDistance", Value: 1000}}}}, false},
+		{"within", "within(a, 'box', [[54.54, 36.36], [55.55, 37.37]])", map[string]interface{}{"a": []interface{}{55, 37}}, bson.M{"a.geometry": bson.M{"$geoWithin": bson.M{"$box": []interface{}{[]interface{}{54.54, 36.36}, []interface{}{55.55, 37.37}}}}}, false},
+		{"time", "d > Time.Date('2021-08-31')", nil, bson.M{"d": bson.M{"$gt": dt}}, false},
+		{"time", fmt.Sprintf("d > Time.Time('%s')", now.Format(time.RFC3339)), nil, bson.M{"d": bson.M{"$gt": tm}}, false},
+		{"in", "In(s, [1,2,3])", nil, bson.M{"s": bson.M{"$in": []interface{}{1, 2, 3}}}, false},
+		{"in", "In(s, 1)", nil, bson.M{"s": bson.M{"$in": []interface{}{1}}}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotB, err := ConvertToMongo(ctx, tt.eval, tt.env, nil)
+			require.NoError(t, err)
+			assert.Equal(t, tt.wantB, gotB)
+		})
+	}
+}
+
+func BenchmarkConvertToMongo(b *testing.B) {
+	const idsNum = 1_000_000
+	ctx := context.Background()
+
+	ids := make([]string, idsNum)
+	for i := 0; i < idsNum; i++ {
+		ids[i] = id.GenerateNewID()
+	}
+	exp := InStringArray("id", ids)
+	//fmt.Println(len(exp))
+
+	for i := 0; i < b.N; i++ {
+		ConvertToMongo(ctx, exp, nil, nil, expr.Patch(&testVisitor{}))
+	}
+}
+
+type testVisitor struct{}
+
+func (v *testVisitor) Enter(node *ast.Node) {}
+func (v *testVisitor) Exit(node *ast.Node) {
+	if n, ok := (*node).(*ast.IdentifierNode); ok {
+		n.Value = "some" + "." + n.Value
+	}
+}
diff --git a/pkg/expr/slice.go b/pkg/expr/slice.go
new file mode 100644
index 0000000000000000000000000000000000000000..d286e146b79a4f430249db5ae13565ffe2da3764
--- /dev/null
+++ b/pkg/expr/slice.go
@@ -0,0 +1,54 @@
+package expr
+
+const (
+	Unknown uint8 = iota
+	String
+	Int
+	Float
+)
+
+func init() {
+	RegisterOption(
+		Extend("In", In),
+	)
+}
+
+func In(v1, v2 interface{}) bool {
+	if v1 == nil || v2 == nil {
+		return false
+	}
+
+	s1, ok1 := v1.([]interface{})
+	s2, ok2 := v2.([]interface{})
+
+	switch {
+	case ok1 && ok2:
+		return sliceInSlice1(s1, s2)
+	case ok1 && !ok2:
+		return valueInSlice1(v2, s1)
+	case !ok1 && ok2:
+		return valueInSlice1(v1, s2)
+	case !ok1 && !ok2:
+		return v1 == v2
+	}
+
+	return false
+}
+
+func sliceInSlice1(s1, s2 []interface{}) bool {
+	for _, v1 := range s1 {
+		if valueInSlice1(v1, s2) {
+			return true
+		}
+	}
+	return false
+}
+
+func valueInSlice1(v interface{}, s []interface{}) bool {
+	for _, e := range s {
+		if e == v {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/expr/slice_test.go b/pkg/expr/slice_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..533c2bebbd29677dfef9951cdbd90596d3c1d027
--- /dev/null
+++ b/pkg/expr/slice_test.go
@@ -0,0 +1,62 @@
+package expr
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/net/context"
+)
+
+func TestSlice(t *testing.T) {
+	ctx := context.Background()
+
+	tests := []struct {
+		name  string
+		eval  string
+		env   map[string]interface{}
+		wantB interface{}
+	}{
+		// nil
+		{"nil#1", "In(a1, a2)", map[string]interface{}{"a1": nil, "a2": nil}, false},
+		{"nil#2", "In(a1, a2)", map[string]interface{}{"a1": nil, "a2": []interface{}{"a", "b"}}, false},
+		{"nil#3", "In(a1, a2)", map[string]interface{}{"a1": nil, "a2": []interface{}{1, 2}}, false},
+		{"nil#4", "In(a1, a2)", map[string]interface{}{"a1": nil, "a2": []interface{}{1.1, 1.2}}, false},
+		{"nil#5", "In(a1, a2)", map[string]interface{}{"a1": nil, "a2": "a"}, false},
+		{"nil#6", "In(a1, a2)", map[string]interface{}{"a1": nil, "a2": 1}, false},
+		{"nil#7", "In(a1, a2)", map[string]interface{}{"a1": nil, "a2": 1.1}, false},
+		// strings
+		{"strings#1", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{"a", "b"}, "a2": []interface{}{"a", "b"}}, true},
+		{"strings#2", "In(a1, a2)", map[string]interface{}{"a1": "b", "a2": []interface{}{"a", "b"}}, true},
+		{"strings#3", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{"a", "b"}, "a2": "b"}, true},
+		{"strings#4", "In(a1, a2)", map[string]interface{}{"a1": "b", "a2": "b"}, true},
+		{"strings#5", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{"a", "b"}, "a2": nil}, false},
+		{"strings#6", "In(a1, a2)", map[string]interface{}{"a1": "c", "a2": []interface{}{"a", "b"}}, false},
+		{"strings#7", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{"a", "b"}, "a2": "c"}, false},
+		{"strings#8", "In(a1, a2)", map[string]interface{}{"a1": "a", "a2": "b"}, false},
+		{"strings#9", "In(a1, a2)", map[string]interface{}{"a1": "1", "a2": 1}, false},
+		// numbers
+		{"numbers#1", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{1, 2}, "a2": []interface{}{1, 2}}, true},
+		{"numbers#5", "In(a1, a2)", map[string]interface{}{"a1": 1, "a2": []interface{}{1, 2}}, true},
+		{"numbers#6", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{1, 2}, "a2": 2}, true},
+		{"numbers#7", "In(a1, a2)", map[string]interface{}{"a1": 3, "a2": []interface{}{1, 2}}, false},
+		{"numbers#8", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{1, 2}, "a2": 3}, false},
+		{"numbers#9", "In(a1, a1)", map[string]interface{}{"a1": []interface{}{1.1, 2.1}, "a2": []interface{}{1.1, 2.1}}, true},
+		{"numbers#10", "In(a1, a2)", map[string]interface{}{"a1": 1.1, "a2": []interface{}{1.1, 2.1}}, true},
+		{"numbers#11", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{1.1, 2.1}, "a2": 2.1}, true},
+		{"numbers#12", "In(a1, a2)", map[string]interface{}{"a1": 3.1, "a2": []interface{}{1.1, 2.1}}, false},
+		{"numbers#13", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{1.1, 2.1}, "a2": 3.1}, false},
+		// mix types
+		{"mix#1", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{"a", "b"}, "a2": map[string]interface{}{"a": "a", "b": "b"}}, false},
+		{"mix#2", "In(a1, a2)", map[string]interface{}{"a1": 2, "a2": "2"}, false},
+		{"mix#3", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{}, "a2": nil}, false},
+		{"mix#4", "In(a1, a2)", map[string]interface{}{"a1": []interface{}{"1", "2"}, "a2": []interface{}{1, 2}}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotB, err := Eval(ctx, tt.eval, tt.env)
+			require.NoError(t, err)
+			assert.Equal(t, tt.wantB, gotB)
+		})
+	}
+}
diff --git a/pkg/expr/string.go b/pkg/expr/string.go
new file mode 100644
index 0000000000000000000000000000000000000000..0e857b41f3d909437f1e8296fd48e866fdac9104
--- /dev/null
+++ b/pkg/expr/string.go
@@ -0,0 +1,50 @@
+package expr
+
+import "strings"
+
+func init() {
+	RegisterOption(
+		Extend("icontains", IContains),
+		Extend("istartsWith", IStartsWith),
+		Extend("iendsWith", IEndsWith),
+	)
+}
+
+func IContains(v1, v2 interface{}) bool {
+	s1, ok := v1.(string)
+	if !ok {
+		return false
+	}
+	s2, ok := v2.(string)
+	if !ok {
+		return false
+	}
+
+	return strings.Contains(strings.ToLower(s1), strings.ToLower(s2))
+}
+
+func IStartsWith(v1, v2 interface{}) bool {
+	s1, ok := v1.(string)
+	if !ok {
+		return false
+	}
+	s2, ok := v2.(string)
+	if !ok {
+		return false
+	}
+
+	return strings.HasPrefix(strings.ToLower(s1), strings.ToLower(s2))
+}
+
+func IEndsWith(v1, v2 interface{}) bool {
+	s1, ok := v1.(string)
+	if !ok {
+		return false
+	}
+	s2, ok := v2.(string)
+	if !ok {
+		return false
+	}
+
+	return strings.HasSuffix(strings.ToLower(s1), strings.ToLower(s2))
+}
diff --git a/pkg/expr/string_test.go b/pkg/expr/string_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ee8e69fada36dfb394d2dd10d3c54d9c950033bf
--- /dev/null
+++ b/pkg/expr/string_test.go
@@ -0,0 +1,84 @@
+package expr
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIContains(t *testing.T) {
+	tests := []struct {
+		name string
+		s1   interface{}
+		s2   interface{}
+		want bool
+	}{
+		{name: "nil#1", s1: nil, s2: nil, want: false},
+		{name: "nil#2", s1: nil, s2: "hello", want: false},
+		{name: "nil#3", s1: "hello", s2: nil, want: false},
+		{name: "notstring#1", s1: map[string]interface{}{"a1": []interface{}{"a", "b"}}, s2: "hello", want: false},
+		{name: "notstring#2", s1: 1, s2: "hello", want: false},
+		{name: "correct#1", s1: "Abc", s2: "abc", want: true},
+		{name: "correct#2", s1: "HELLO", s2: "hello", want: true},
+		{name: "correct#3", s1: "abcDeFGh", s2: "dEFg", want: true},
+		{name: "correct#4", s1: "heLLOworLD", s2: "ORL", want: true},
+		{name: "incorrect#1", s1: "HELLO", s2: "elloo", want: false},
+		{name: "incorrect#2", s1: "world", s2: "Word", want: false},
+		{name: "incorrect#3", s1: "heLLOworLD", s2: "OOo", want: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, IContains(tt.s1, tt.s2), "Icontains(%v, %v)", tt.s1, tt.s2)
+		})
+	}
+}
+
+func TestIEndsWith(t *testing.T) {
+	tests := []struct {
+		name string
+		s1   interface{}
+		s2   interface{}
+		want bool
+	}{
+		{name: "nil#1", s1: nil, s2: nil, want: false},
+		{name: "nil#2", s1: nil, s2: "hello", want: false},
+		{name: "nil#3", s1: "hello", s2: nil, want: false},
+		{name: "notstring#1", s1: map[string]interface{}{"a1": []interface{}{"a", "b"}}, s2: "hello", want: false},
+		{name: "notstring#2", s1: 1, s2: "hello", want: false},
+		{name: "correct#1", s1: "helloabc", s2: "Abc", want: true},
+		{name: "correct#2", s1: "worldhello", s2: "HELLO", want: true},
+		{name: "correct#3", s1: "abcDeFGh", s2: "FGH", want: true},
+		{name: "incorrect#1", s1: "ello", s2: "HeLLo", want: false},
+		{name: "incorrect#2", s1: "Word", s2: "world", want: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, IEndsWith(tt.s1, tt.s2), "IendsWith(%v, %v)", tt.s1, tt.s2)
+		})
+	}
+}
+
+func TestIStartsWith(t *testing.T) {
+	tests := []struct {
+		name string
+		s1   interface{}
+		s2   interface{}
+		want bool
+	}{
+		{name: "nil#1", s1: nil, s2: nil, want: false},
+		{name: "nil#2", s1: nil, s2: "hello", want: false},
+		{name: "nil#3", s1: "hello", s2: nil, want: false},
+		{name: "notstring#1", s1: map[string]interface{}{"a1": []interface{}{"a", "b"}}, s2: "hello", want: false},
+		{name: "notstring#2", s1: 1, s2: "hello", want: false},
+		{name: "correct#1", s1: "helloabc", s2: "he", want: true},
+		{name: "correct#2", s1: "worldhello", s2: "WOR", want: true},
+		{name: "correct#3", s1: "abcDeFGh", s2: "ABC", want: true},
+		{name: "incorrect#1", s1: "ello", s2: "HeLLo", want: false},
+		{name: "incorrect#2", s1: "Word", s2: "world", want: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, IStartsWith(tt.s1, tt.s2), "IstartsWith(%v, %v)", tt.s1, tt.s2)
+		})
+	}
+}
diff --git a/pkg/expr/time.go b/pkg/expr/time.go
new file mode 100644
index 0000000000000000000000000000000000000000..740da301a70b65a15f83ac70b1f512c47c171b35
--- /dev/null
+++ b/pkg/expr/time.go
@@ -0,0 +1,79 @@
+package expr
+
+import (
+	"time"
+
+	"github.com/antonmedv/expr"
+)
+
+const DefaultTimeLayout = time.RFC3339
+const DefaultDateLayout = "2006-01-02"
+
+type TimeExpr struct{}
+
+func init() {
+	RegisterOption(
+		Extend("Time", TimeExpr{}),
+		expr.Operator("==", "Time.Equal"),
+		expr.Operator("<", "Time.Before"),
+		expr.Operator(">", "Time.After"),
+		expr.Operator("<=", "Time.BeforeOrEqual"),
+		expr.Operator(">", "Time.After"),
+		expr.Operator(">=", "Time.AfterOrEqual"),
+
+		// Time and duration manipulation.
+		expr.Operator("+", "Time.Add"),
+		expr.Operator("-", "Time.Sub"),
+
+		// Operators override for duration comprising.
+		expr.Operator("==", "Time.EqualDuration"),
+		expr.Operator("<", "Time.BeforeDuration"),
+		expr.Operator("<=", "Time.BeforeOrEqualDuration"),
+		expr.Operator(">", "Time.AfterDuration"),
+		expr.Operator(">=", "Time.AfterOrEqualDuration"),
+	)
+}
+
+func (TimeExpr) Time(v interface{}) time.Time {
+	switch val := v.(type) {
+
+	case string:
+		if t, err := time.Parse(DefaultTimeLayout, val); err != nil {
+			panic(err)
+		} else {
+			return t
+		}
+	case time.Time:
+		return v.(time.Time)
+	}
+	return v.(time.Time)
+}
+
+func (TimeExpr) Date(s string) time.Time {
+	t, err := time.Parse(DefaultDateLayout, s)
+	if err != nil {
+		panic(err)
+	}
+	return t
+}
+func (TimeExpr) Duration(s string) time.Duration {
+	d, err := time.ParseDuration(s)
+	if err != nil {
+		panic(err)
+	}
+	return d
+}
+
+func (TimeExpr) Now() time.Time                                { return time.Now() }
+func (TimeExpr) Equal(a, b time.Time) bool                     { return a.Equal(b) }
+func (TimeExpr) Before(a, b time.Time) bool                    { return a.Before(b) }
+func (TimeExpr) BeforeOrEqual(a, b time.Time) bool             { return a.Before(b) || a.Equal(b) }
+func (TimeExpr) After(a, b time.Time) bool                     { return a.After(b) }
+func (TimeExpr) AfterOrEqual(a, b time.Time) bool              { return a.After(b) || a.Equal(b) }
+func (TimeExpr) Add(a time.Time, b time.Duration) time.Time    { return a.Add(b) }
+func (TimeExpr) Sub(a, b time.Time) time.Duration              { return a.Sub(b) }
+func (TimeExpr) EqualDuration(a, b time.Duration) bool         { return a == b }
+func (TimeExpr) BeforeDuration(a, b time.Duration) bool        { return a < b }
+func (TimeExpr) BeforeOrEqualDuration(a, b time.Duration) bool { return a <= b }
+func (TimeExpr) AfterDuration(a, b time.Duration) bool         { return a > b }
+func (TimeExpr) AfterOrEqualDuration(a, b time.Duration) bool  { return a >= b }
diff --git a/pkg/id/id.go b/pkg/id/id.go
new file mode 100644
index 0000000000000000000000000000000000000000..cfb8b6390ac48e50ed5fb899db11316b3f1df31c
--- /dev/null
+++ b/pkg/id/id.go
@@ -0,0 +1,7 @@
+package id
+
+import "github.com/rs/xid"
+
+func GenerateNewID() string {
+	return xid.New().String()
+}
diff --git a/pkg/locales/locale.go b/pkg/locales/locale.go
new file mode 100644
index 0000000000000000000000000000000000000000..d3cc43c6c625a5090d975409d7aa2beb16eefc3a
--- /dev/null
+++ b/pkg/locales/locale.go
@@ -0,0 +1,7 @@
+package locales
+
+type Locale struct {
+	ID      string `json:"id" bson:"_id"` // (Пример: "en", "en-US")
+	SpaceID string `json:"spaceId" bson:"-"`
+	Name    string `json:"name" bson:"name"` // (Пример: "English", "English (US)" )
+}
diff --git a/pkg/locales/mocks/Locales.go b/pkg/locales/mocks/Locales.go
new file mode 100644
index 0000000000000000000000000000000000000000..3e63dfedfff975f52f886d19e1e8982bccb96b7e
--- /dev/null
+++ b/pkg/locales/mocks/Locales.go
@@ -0,0 +1,75 @@
+// Code generated by mockery v2.7.4. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	locales "git.perx.ru/perxis/perxis-go/pkg/locales"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Locales is an autogenerated mock type for the Locales type
+type Locales struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, locale
+func (_m *Locales) Create(ctx context.Context, locale *locales.Locale) (*locales.Locale, error) {
+	ret := _m.Called(ctx, locale)
+
+	var r0 *locales.Locale
+	if rf, ok := ret.Get(0).(func(context.Context, *locales.Locale) *locales.Locale); ok {
+		r0 = rf(ctx, locale)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*locales.Locale)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *locales.Locale) error); ok {
+		r1 = rf(ctx, locale)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, spaceId, localeId
+func (_m *Locales) Delete(ctx context.Context, spaceId string, localeId string) error {
+	ret := _m.Called(ctx, spaceId, localeId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceId, localeId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// List provides a mock function with given fields: ctx, spaceId
+func (_m *Locales) List(ctx context.Context, spaceId string) ([]*locales.Locale, error) {
+	ret := _m.Called(ctx, spaceId)
+
+	var r0 []*locales.Locale
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*locales.Locale); ok {
+		r0 = rf(ctx, spaceId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*locales.Locale)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, spaceId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
diff --git a/pkg/locales/service.go b/pkg/locales/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..7724d7f7ecdb2f4733d97bff01113d105adabdeb
--- /dev/null
+++ b/pkg/locales/service.go
@@ -0,0 +1,14 @@
+package locales
+
+import (
+	"context"
+)
+
+// @microgen grpc
+// @protobuf git.perx.ru/perxis/perxis-go/proto/locales
+// @grpc-addr content.locales.Locales
+type Locales interface {
+	Create(ctx context.Context, locale *Locale) (created *Locale, err error)
+	List(ctx context.Context, spaceId string) (locales []*Locale, err error)
+	Delete(ctx context.Context, spaceId, localeId string) (err error)
+}
diff --git a/pkg/locales/transport/client.microgen.go b/pkg/locales/transport/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..f8cd9dee23dafeff1d635b3f5ee42f6e5c8667f1
--- /dev/null
+++ b/pkg/locales/transport/client.microgen.go
@@ -0,0 +1,51 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	"errors"
+
+	locales "git.perx.ru/perxis/perxis-go/pkg/locales"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+func (set EndpointsSet) Create(arg0 context.Context, arg1 *locales.Locale) (res0 *locales.Locale, res1 error) {
+	request := CreateRequest{Locale: arg1}
+	response, res1 := set.CreateEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*CreateResponse).Created, res1
+}
+
+func (set EndpointsSet) List(arg0 context.Context, arg1 string) (res0 []*locales.Locale, res1 error) {
+	request := ListRequest{SpaceId: arg1}
+	response, res1 := set.ListEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*ListResponse).Locales, res1
+}
+
+func (set EndpointsSet) Delete(arg0 context.Context, arg1 string, arg2 string) (res0 error) {
+	request := DeleteRequest{
+		LocaleId: arg2,
+		SpaceId:  arg1,
+	}
+	_, res0 = set.DeleteEndpoint(arg0, &request)
+	if res0 != nil {
+		if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res0 = errors.New(e.Message())
+		}
+		return
+	}
+	return res0
+}
diff --git a/pkg/locales/transport/endpoints.microgen.go b/pkg/locales/transport/endpoints.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..ffca7318747104f8b58af332e90646f8cc6a8b9c
--- /dev/null
+++ b/pkg/locales/transport/endpoints.microgen.go
@@ -0,0 +1,12 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import endpoint "github.com/go-kit/kit/endpoint"
+
+// EndpointsSet implements Locales API and used for transport purposes.
+type EndpointsSet struct {
+	CreateEndpoint endpoint.Endpoint
+	ListEndpoint   endpoint.Endpoint
+	DeleteEndpoint endpoint.Endpoint
+}
diff --git a/pkg/locales/transport/exchanges.microgen.go b/pkg/locales/transport/exchanges.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..a07204e13a233fd7bae40c74e9984061871a92b7
--- /dev/null
+++ b/pkg/locales/transport/exchanges.microgen.go
@@ -0,0 +1,28 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import locales "git.perx.ru/perxis/perxis-go/pkg/locales"
+
+type (
+	CreateRequest struct {
+		Locale *locales.Locale `json:"locale"`
+	}
+	CreateResponse struct {
+		Created *locales.Locale `json:"created"`
+	}
+
+	ListRequest struct {
+		SpaceId string `json:"space_id"`
+	}
+	ListResponse struct {
+		Locales []*locales.Locale `json:"locales"`
+	}
+
+	DeleteRequest struct {
+		SpaceId  string `json:"space_id"`
+		LocaleId string `json:"locale_id"`
+	}
+	// Formal exchange type, please do not delete.
+	DeleteResponse struct{}
+)
diff --git a/pkg/locales/transport/grpc/client.microgen.go b/pkg/locales/transport/grpc/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..3af5bbae0ed86bf079d93622cc8f1aa4f7fbdfd8
--- /dev/null
+++ b/pkg/locales/transport/grpc/client.microgen.go
@@ -0,0 +1,40 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/locales/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/locales"
+	grpckit "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	grpc "google.golang.org/grpc"
+)
+
+func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet {
+	if addr == "" {
+		addr = "content.locales.Locales"
+	}
+	return transport.EndpointsSet{
+		CreateEndpoint: grpckit.NewClient(
+			conn, addr, "Create",
+			_Encode_Create_Request,
+			_Decode_Create_Response,
+			pb.CreateResponse{},
+			opts...,
+		).Endpoint(),
+		DeleteEndpoint: grpckit.NewClient(
+			conn, addr, "Delete",
+			_Encode_Delete_Request,
+			_Decode_Delete_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+		ListEndpoint: grpckit.NewClient(
+			conn, addr, "List",
+			_Encode_List_Request,
+			_Decode_List_Response,
+			pb.ListResponse{},
+			opts...,
+		).Endpoint(),
+	}
+}
diff --git a/pkg/locales/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/locales/transport/grpc/protobuf_endpoint_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..5d06e8232c89f226e4f7c0a80ca6d52eb219d48f
--- /dev/null
+++ b/pkg/locales/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -0,0 +1,131 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// Please, do not change functions names!
+package transportgrpc
+
+import (
+	"context"
+	"errors"
+
+	transport "git.perx.ru/perxis/perxis-go/pkg/locales/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/locales"
+	empty "github.com/golang/protobuf/ptypes/empty"
+)
+
+func _Encode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil CreateRequest")
+	}
+	req := request.(*transport.CreateRequest)
+	pbLocale, err := PtrLocaleToProto(req.Locale)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateRequest{Locale: pbLocale}, nil
+}
+
+func _Encode_List_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListRequest")
+	}
+	req := request.(*transport.ListRequest)
+	return &pb.ListRequest{SpaceId: req.SpaceId}, nil
+}
+
+func _Encode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil DeleteRequest")
+	}
+	req := request.(*transport.DeleteRequest)
+	return &pb.DeleteRequest{
+		LocaleId: req.LocaleId,
+		SpaceId:  req.SpaceId,
+	}, nil
+}
+
+func _Encode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil CreateResponse")
+	}
+	resp := response.(*transport.CreateResponse)
+	respLocale, err := PtrLocaleToProto(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateResponse{Locale: respLocale}, nil
+}
+
+func _Encode_List_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListResponse")
+	}
+	resp := response.(*transport.ListResponse)
+	respLocales, err := ListPtrLocaleToProto(resp.Locales)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.ListResponse{Locales: respLocales}, nil
+}
+
+func _Encode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil CreateRequest")
+	}
+	req := request.(*pb.CreateRequest)
+	locale, err := ProtoToPtrLocale(req.Locale)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateRequest{Locale: locale}, nil
+}
+
+func _Decode_List_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListRequest")
+	}
+	req := request.(*pb.ListRequest)
+	return &transport.ListRequest{SpaceId: string(req.SpaceId)}, nil
+}
+
+func _Decode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil DeleteRequest")
+	}
+	req := request.(*pb.DeleteRequest)
+	return &transport.DeleteRequest{
+		LocaleId: string(req.LocaleId),
+		SpaceId:  string(req.SpaceId),
+	}, nil
+}
+
+func _Decode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil CreateResponse")
+	}
+	resp := response.(*pb.CreateResponse)
+	respLocale, err := ProtoToPtrLocale(resp.Locale)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateResponse{Created: respLocale}, nil
+}
+
+func _Decode_List_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListResponse")
+	}
+	resp := response.(*pb.ListResponse)
+	respLocales, err := ProtoToListPtrLocale(resp.Locales)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.ListResponse{Locales: respLocales}, nil
+}
+
+func _Decode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
diff --git a/pkg/locales/transport/grpc/protobuf_type_converters.microgen.go b/pkg/locales/transport/grpc/protobuf_type_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..6dca0bb8afa68dfe3fa17b53d315f3716c48ae36
--- /dev/null
+++ b/pkg/locales/transport/grpc/protobuf_type_converters.microgen.go
@@ -0,0 +1,48 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// It is better for you if you do not change functions names!
+// This file will never be overwritten.
+package transportgrpc
+
+import (
+	service "git.perx.ru/perxis/perxis-go/pkg/locales"
+	pb "git.perx.ru/perxis/perxis-go/proto/locales"
+)
+
+func PtrLocaleToProto(locale *service.Locale) (*pb.Locale, error) {
+	if locale == nil {
+		return nil, nil
+	}
+	return &pb.Locale{Id: locale.ID, Name: locale.Name, SpaceId: locale.SpaceID}, nil
+}
+
+func ProtoToPtrLocale(protoLocale *pb.Locale) (*service.Locale, error) {
+	if protoLocale == nil {
+		return nil, nil
+	}
+	return &service.Locale{ID: protoLocale.Id, Name: protoLocale.Name, SpaceID: protoLocale.SpaceId}, nil
+}
+
+func ListPtrLocaleToProto(locales []*service.Locale) ([]*pb.Locale, error) {
+	protoLocales := make([]*pb.Locale, 0, len(locales))
+	for _, l := range locales {
+		pl, err := PtrLocaleToProto(l)
+		if err != nil {
+			return nil, err
+		}
+		protoLocales = append(protoLocales, pl)
+	}
+	return protoLocales, nil
+}
+
+func ProtoToListPtrLocale(protoLocales []*pb.Locale) ([]*service.Locale, error) {
+	locales := make([]*service.Locale, 0, len(protoLocales))
+	for _, pl := range protoLocales {
+		l, err := ProtoToPtrLocale(pl)
+		if err != nil {
+			return nil, err
+		}
+		locales = append(locales, l)
+	}
+	return locales, nil
+}
diff --git a/pkg/locales/transport/grpc/server.microgen.go b/pkg/locales/transport/grpc/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..88e549f6e23e99a04d037fcefb4603a13821d24a
--- /dev/null
+++ b/pkg/locales/transport/grpc/server.microgen.go
@@ -0,0 +1,67 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// DO NOT EDIT.
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/locales/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/locales"
+	grpc "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	context "golang.org/x/net/context"
+)
+
+type localesServer struct {
+	create grpc.Handler
+	list   grpc.Handler
+	delete grpc.Handler
+
+	pb.UnimplementedLocalesServer
+}
+
+func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.LocalesServer {
+	return &localesServer{
+		create: grpc.NewServer(
+			endpoints.CreateEndpoint,
+			_Decode_Create_Request,
+			_Encode_Create_Response,
+			opts...,
+		),
+		delete: grpc.NewServer(
+			endpoints.DeleteEndpoint,
+			_Decode_Delete_Request,
+			_Encode_Delete_Response,
+			opts...,
+		),
+		list: grpc.NewServer(
+			endpoints.ListEndpoint,
+			_Decode_List_Request,
+			_Encode_List_Response,
+			opts...,
+		),
+	}
+}
+
+func (S *localesServer) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) {
+	_, resp, err := S.create.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.CreateResponse), nil
+}
+
+func (S *localesServer) List(ctx context.Context, req *pb.ListRequest) (*pb.ListResponse, error) {
+	_, resp, err := S.list.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.ListResponse), nil
+}
+
+func (S *localesServer) Delete(ctx context.Context, req *pb.DeleteRequest) (*empty.Empty, error) {
+	_, resp, err := S.delete.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
diff --git a/pkg/locales/transport/server.microgen.go b/pkg/locales/transport/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..5ce815dcb52415a93ddfeb095f99e22fb14c4492
--- /dev/null
+++ b/pkg/locales/transport/server.microgen.go
@@ -0,0 +1,42 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+
+	locales "git.perx.ru/perxis/perxis-go/pkg/locales"
+	endpoint "github.com/go-kit/kit/endpoint"
+)
+
+func Endpoints(svc locales.Locales) EndpointsSet {
+	return EndpointsSet{
+		CreateEndpoint: CreateEndpoint(svc),
+		DeleteEndpoint: DeleteEndpoint(svc),
+		ListEndpoint:   ListEndpoint(svc),
+	}
+}
+
+func CreateEndpoint(svc locales.Locales) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*CreateRequest)
+		res0, res1 := svc.Create(arg0, req.Locale)
+		return &CreateResponse{Created: res0}, res1
+	}
+}
+
+func ListEndpoint(svc locales.Locales) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*ListRequest)
+		res0, res1 := svc.List(arg0, req.SpaceId)
+		return &ListResponse{Locales: res0}, res1
+	}
+}
+
+func DeleteEndpoint(svc locales.Locales) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*DeleteRequest)
+		res0 := svc.Delete(arg0, req.SpaceId, req.LocaleId)
+		return &DeleteResponse{}, res0
+	}
+}
diff --git a/pkg/options/options.go b/pkg/options/options.go
new file mode 100644
index 0000000000000000000000000000000000000000..7f8b0cd624db7e6c4c35fe62333a67ba633fbe7e
--- /dev/null
+++ b/pkg/options/options.go
@@ -0,0 +1,122 @@
+package options
+
+import "time"
+
+// SortOptions настройки сортировки результатов
+type SortOptions struct {
+	Sort []string
+}
+
+// PaginationOptions настройки возвращаемых страниц результатов
+type PaginationOptions struct {
+	PageNum  int
+	PageSize int
+}
+
+// FieldOptions настройки включения/исключения полей из результатов запроса
+type FieldOptions struct {
+	// Fields - Наименования полей для включения/исключения из результатов запроса (только указанные поля)
+	// Если `ExcludeFields` не установлен, то результат содержит только указанные поля
+	// Если `ExcludeFields` установлен, то результат содержит все поля кроме указанных
+	Fields []string
+
+	// ExcludeFields- Если флаг установлен, то перечисленные поля `Fields` следует исключить из результатов
+	ExcludeFields bool
+}
+
+// FindOptions настройки возвращаемых результатов поиска
+type FindOptions struct {
+	SortOptions
+	PaginationOptions
+	FieldOptions
+}
+
+// NewFindOptions создает новые результаты поиска
+func NewFindOptions(pageNum, pageSize int, sort ...string) *FindOptions {
+	return &FindOptions{
+		PaginationOptions: PaginationOptions{
+			PageNum:  pageNum,
+			PageSize: pageSize,
+		},
+		SortOptions: SortOptions{
+			Sort: sort,
+		},
+	}
+}
+
+// MergeFindOptions объединяет в FindOptions различные варианты настроек
+func MergeFindOptions(opts ...interface{}) *FindOptions {
+	fo := &FindOptions{}
+	for _, opt := range opts {
+		if opt == nil {
+			continue
+		}
+
+		switch o := opt.(type) {
+		case FindOptions:
+			fo.SortOptions = MergeSortOptions(fo.SortOptions, o.SortOptions)
+			fo.PaginationOptions = MergePaginationOptions(fo.PaginationOptions, o.PaginationOptions)
+			fo.FieldOptions = MergeFieldOptions(fo.FieldOptions, o.FieldOptions)
+		case *FindOptions:
+			fo.SortOptions = MergeSortOptions(fo.SortOptions, o.SortOptions)
+			fo.PaginationOptions = MergePaginationOptions(fo.PaginationOptions, o.PaginationOptions)
+			fo.FieldOptions = MergeFieldOptions(fo.FieldOptions, o.FieldOptions)
+		case SortOptions:
+			fo.SortOptions = MergeSortOptions(fo.SortOptions, o)
+		case *SortOptions:
+			fo.SortOptions = MergeSortOptions(fo.SortOptions, *o)
+		case PaginationOptions:
+			fo.PaginationOptions = MergePaginationOptions(fo.PaginationOptions, o)
+		case *PaginationOptions:
+			fo.PaginationOptions = MergePaginationOptions(fo.PaginationOptions, *o)
+		case FieldOptions:
+			fo.FieldOptions = o
+		case *FieldOptions:
+			fo.FieldOptions = *o
+		}
+	}
+	return fo
+}
+
+type TimeFilter struct {
+	Before, After time.Time
+}
+
+// MergeSortOptions объединяет настройки сортировки
+func MergeSortOptions(options ...SortOptions) SortOptions {
+	fo := SortOptions{}
+	for _, opt := range options {
+		if len(opt.Sort) == 0 {
+			continue
+		}
+		fo.Sort = append(fo.Sort, opt.Sort...)
+	}
+	return fo
+}
+
+// MergePaginationOptions объединяет настройки страниц
+func MergePaginationOptions(options ...PaginationOptions) PaginationOptions {
+	fo := PaginationOptions{}
+	for _, opt := range options {
+		if opt.PageSize == 0 && opt.PageNum == 0 {
+			continue
+		}
+		fo.PageNum = opt.PageNum
+		fo.PageSize = opt.PageSize
+	}
+	return fo
+}
+
+// MergeFieldOptions выполняет слияние опций для возвращаемых полей.
+// Выбирается не пустое значение.
+func MergeFieldOptions(options ...FieldOptions) FieldOptions {
+	fo := FieldOptions{}
+	for _, opt := range options {
+		if len(opt.Fields) > 0 {
+			fo.Fields = opt.Fields
+			fo.ExcludeFields = opt.ExcludeFields
+			return fo
+		}
+	}
+	return fo
+}
diff --git a/pkg/options/options_test.go b/pkg/options/options_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..981849a0e2625a8bd7e796a22eaa8529d606ef01
--- /dev/null
+++ b/pkg/options/options_test.go
@@ -0,0 +1,60 @@
+package options
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestOptions_MergePaginationOptions(t *testing.T) {
+
+	var tt = []struct {
+		name     string
+		options  []PaginationOptions
+		expected PaginationOptions
+	}{
+		{
+			name:     "Nil option",
+			options:  nil,
+			expected: PaginationOptions{},
+		},
+		{
+			name:     "Empty options",
+			options:  []PaginationOptions{},
+			expected: PaginationOptions{},
+		},
+		{
+			name:     "One option",
+			options:  []PaginationOptions{{PageNum: 10, PageSize: 100}},
+			expected: PaginationOptions{PageNum: 10, PageSize: 100},
+		},
+		{
+			name:     "Merge #1",
+			options:  []PaginationOptions{{PageNum: 0, PageSize: 0}, {PageNum: 10, PageSize: 100}},
+			expected: PaginationOptions{PageNum: 10, PageSize: 100},
+		},
+		{
+			name:     "Merge #2",
+			options:  []PaginationOptions{{PageNum: 10, PageSize: 100}, {PageNum: 0, PageSize: 0}},
+			expected: PaginationOptions{PageNum: 10, PageSize: 100},
+		},
+		{
+			name:     "Merge #3",
+			options:  []PaginationOptions{{PageNum: 0, PageSize: 0}, {PageNum: 10, PageSize: 100}, {PageNum: 0, PageSize: 0}},
+			expected: PaginationOptions{PageNum: 10, PageSize: 100},
+		},
+		{
+			name:     "Merge #4",
+			options:  []PaginationOptions{{PageNum: 10, PageSize: 100}, {}},
+			expected: PaginationOptions{PageNum: 10, PageSize: 100},
+		},
+	}
+
+	for _, v := range tt {
+
+		t.Run(v.name, func(t *testing.T) {
+			actual := MergePaginationOptions(v.options...)
+			assert.Equal(t, v.expected, actual)
+		})
+	}
+}
diff --git a/pkg/permission/permission.go b/pkg/permission/permission.go
new file mode 100644
index 0000000000000000000000000000000000000000..7a90e5079da08648cbdefe565d39f4d8b801a275
--- /dev/null
+++ b/pkg/permission/permission.go
@@ -0,0 +1,64 @@
+package permission
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/data"
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
+)
+
+type Access uint64
+
+const (
+	AccessAny  = iota // Access to any items (default)
+	AccessMine        // Access to items owned by me (future use)
+	AccessRole        // Access to items owned by my role (future use)
+)
+
+type Permission struct {
+	Permitted       bool
+	UnallowedFields []string
+	Filter          string
+}
+
+func (p Permission) Can() bool {
+	return p.Permitted
+}
+
+func Merge(pp ...*Permission) *Permission {
+
+	res := new(Permission)
+
+	var filters []string
+
+	res.Permitted = true
+	for _, p := range pp {
+		res.Permitted = res.Permitted && p.Permitted
+
+		if p.Filter != "" {
+			filters = append(filters, p.Filter)
+		}
+
+		for _, fld := range p.UnallowedFields {
+			if !data.Contains(fld, res.UnallowedFields) {
+				res.UnallowedFields = append(res.UnallowedFields, fld)
+			}
+		}
+	}
+
+	res.Filter = expr.And(data.SetFromSlice(filters)...)
+
+	return res
+}
+
+func (p Permission) RemoveFields(in map[string]interface{}) map[string]interface{} {
+	if in == nil {
+		return nil
+	}
+	out := make(map[string]interface{})
+	for k, v := range in {
+		if data.Contains(k, p.UnallowedFields) {
+			continue
+		}
+		out[k] = v
+	}
+	return out
+}
diff --git a/pkg/permission/ruleset.go b/pkg/permission/ruleset.go
new file mode 100644
index 0000000000000000000000000000000000000000..b9a84f3747223cf4139c905e3d65ebb9f4dea336
--- /dev/null
+++ b/pkg/permission/ruleset.go
@@ -0,0 +1,266 @@
+package permission
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/data"
+	perxisexpr "git.perx.ru/perxis/perxis-go/pkg/expr"
+)
+
+type Action uint64
+
+const (
+	//ActionAny Action = iota
+	ActionCreate Action = iota + 1
+	ActionRead
+	ActionUpdate
+	ActionDelete
+)
+
+// Rule - правило доступа к контенту
+type Rule struct {
+	CollectionID string   `json:"collectionId" bson:"collectionId,omitempty"`
+	Actions      []Action `json:"actions" bson:"actions,omitempty"`
+	Access       Access   `json:"access" bson:"access,omitempty"`
+	// Поля не передаются API клиенту
+	HiddenFields []string `json:"hiddenFields,omitempty" bson:"hiddenFields,omitempty"`
+	// Клиент не может сохранять данные поля
+	ReadonlyFields []string `json:"readonlyFields,omitempty" bson:"readonlyFields,omitempty"`
+	// Клиент может сохранить данные поля, но поля не передаются в API
+	WriteonlyFields []string `json:"writeonlyFields,omitempty" bson:"writeonlyFields,omitempty"`
+	// Дополнительный фильтр
+	ReadFilter  string `json:"readFilter,omitempty" bson:"readFilter,omitempty"`
+	WriteFilter string `json:"writeFilter,omitempty" bson:"writeFilter,omitempty"`
+}
+
+func NewRule(collectionID string, actions ...Action) *Rule {
+	return &Rule{
+		CollectionID: collectionID,
+		Actions:      actions,
+	}
+}
+
+func (r Rule) Clone() *Rule {
+	return &Rule{
+		CollectionID:    r.CollectionID,
+		Actions:         append([]Action(nil), r.Actions...),
+		Access:          r.Access,
+		HiddenFields:    append([]string(nil), r.HiddenFields...),
+		ReadonlyFields:  append([]string(nil), r.ReadonlyFields...),
+		WriteonlyFields: append([]string(nil), r.WriteonlyFields...),
+		ReadFilter:      r.ReadFilter,
+		WriteFilter:     r.WriteFilter,
+	}
+}
+
+func (r Rule) WithReadFilter(f string) *Rule {
+	return &Rule{
+		CollectionID:    r.CollectionID,
+		Actions:         append([]Action(nil), r.Actions...),
+		Access:          r.Access,
+		HiddenFields:    append([]string(nil), r.HiddenFields...),
+		ReadonlyFields:  append([]string(nil), r.ReadonlyFields...),
+		WriteonlyFields: append([]string(nil), r.WriteonlyFields...),
+		ReadFilter:      f,
+		WriteFilter:     r.WriteFilter,
+	}
+}
+
+func (r Rule) WithWriteFilter(f string) *Rule {
+	return &Rule{
+		CollectionID:    r.CollectionID,
+		Actions:         append([]Action(nil), r.Actions...),
+		Access:          r.Access,
+		HiddenFields:    append([]string(nil), r.HiddenFields...),
+		ReadonlyFields:  append([]string(nil), r.ReadonlyFields...),
+		WriteonlyFields: append([]string(nil), r.WriteonlyFields...),
+		ReadFilter:      r.ReadFilter,
+		WriteFilter:     f,
+	}
+}
+
+func (r Rule) WithReadWriteFilter(f string) *Rule {
+	return &Rule{
+		CollectionID:    r.CollectionID,
+		Actions:         append([]Action(nil), r.Actions...),
+		Access:          r.Access,
+		HiddenFields:    append([]string(nil), r.HiddenFields...),
+		ReadonlyFields:  append([]string(nil), r.ReadonlyFields...),
+		WriteonlyFields: append([]string(nil), r.WriteonlyFields...),
+		ReadFilter:      f,
+		WriteFilter:     f,
+	}
+}
+
+func (r Rule) WithReadonlyFields(ff ...string) *Rule {
+	return &Rule{
+		CollectionID:    r.CollectionID,
+		Actions:         append([]Action(nil), r.Actions...),
+		Access:          r.Access,
+		HiddenFields:    append([]string(nil), r.HiddenFields...),
+		ReadonlyFields:  append(ff, r.ReadonlyFields...),
+		WriteonlyFields: append([]string(nil), r.WriteonlyFields...),
+		ReadFilter:      r.ReadFilter,
+		WriteFilter:     r.WriteFilter,
+	}
+}
+
+func (r Rule) WithHiddenFields(ff ...string) *Rule {
+	return &Rule{
+		CollectionID:    r.CollectionID,
+		Actions:         append([]Action(nil), r.Actions...),
+		Access:          r.Access,
+		HiddenFields:    append(ff, r.HiddenFields...),
+		ReadonlyFields:  append([]string(nil), r.ReadonlyFields...),
+		WriteonlyFields: append([]string(nil), r.WriteonlyFields...),
+		ReadFilter:      r.ReadFilter,
+		WriteFilter:     r.WriteFilter,
+	}
+}
+
+func (r Rule) GetPermission(action Action) *Permission {
+	for _, a := range r.Actions {
+		if a == action {
+			p := &Permission{
+				Permitted: true,
+			}
+
+			switch action {
+			case ActionRead:
+				p.Filter = r.ReadFilter
+				p.UnallowedFields = append(p.UnallowedFields, r.HiddenFields...)
+				p.UnallowedFields = append(p.UnallowedFields, r.WriteonlyFields...)
+			case ActionCreate, ActionUpdate, ActionDelete:
+				p.Filter = r.WriteFilter
+				p.UnallowedFields = append(p.UnallowedFields, r.ReadonlyFields...)
+			}
+
+			return p
+		}
+	}
+
+	return &Permission{}
+}
+
+type Ruleset interface {
+	GetRule(collectionID string) *Rule
+	Permission(collectionID string, action Action) *Permission
+}
+
+type Rules []*Rule
+
+func (r Rules) Permission(collectionID string, action Action) *Permission {
+	rule := r.GetRule(collectionID)
+	return rule.GetPermission(action)
+}
+
+func (r Rules) GetRule(collectionID string) *Rule {
+	for _, rule := range r {
+		if data.GlobMatch(collectionID, rule.CollectionID) {
+			return rule
+		}
+	}
+	return nil
+}
+
+func MergeRules(src, in Rules) Rules {
+	dst := make(Rules, 0, len(src)+len(in))
+	seen := make(map[string]struct{})
+
+	for _, rule := range src {
+		if _, ok := seen[rule.CollectionID]; !ok {
+			dst = append(dst, rule)
+			seen[rule.CollectionID] = struct{}{}
+		}
+	}
+
+	for _, rule := range in {
+		if _, ok := seen[rule.CollectionID]; !ok {
+			dst = append(dst, rule)
+			seen[rule.CollectionID] = struct{}{}
+		}
+	}
+
+	return dst
+}
+
+// MergeRule объединяет несколько Rule в один
+//   - пересечение действий
+//   - объединение hidden, readOnly, writeOnly fields
+//   - объединение фильтров
+func MergeRule(rules ...*Rule) *Rule {
+
+	if len(rules) == 0 {
+		return nil
+	}
+
+	var result *Rule
+	var writeFilter []string
+	var readFilter []string
+
+	for i, r := range rules {
+		if i == 0 { // first element
+
+			result = r.Clone()
+
+			result.CollectionID = ""
+			if result.WriteFilter != "" {
+				writeFilter = append(writeFilter, result.WriteFilter)
+			}
+			if result.ReadFilter != "" {
+				readFilter = append(readFilter, result.ReadFilter)
+			}
+
+			continue
+		}
+
+		result.Actions = data.GetIntersection(result.Actions, r.Actions)
+		result.HiddenFields = data.SetFromSlice(append(result.HiddenFields, r.HiddenFields...))
+		result.ReadonlyFields = data.SetFromSlice(append(result.ReadonlyFields, r.ReadonlyFields...))
+		result.WriteonlyFields = data.SetFromSlice(append(result.WriteonlyFields, r.WriteonlyFields...))
+		if r.WriteFilter != "" {
+			writeFilter = append(writeFilter, r.WriteFilter)
+		}
+		if r.ReadFilter != "" {
+			readFilter = append(readFilter, r.ReadFilter)
+		}
+	}
+
+	result.WriteFilter = perxisexpr.And(data.SetFromSlice(writeFilter)...)
+	result.ReadFilter = perxisexpr.And(data.SetFromSlice(readFilter)...)
+
+	return result
+}
+
+type PrivilegedRuleset struct{}
+
+func (r PrivilegedRuleset) Permission(_ string, _ Action) *Permission {
+	return &Permission{
+		Permitted:       true,
+		UnallowedFields: []string{},
+	}
+}
+
+func (r PrivilegedRuleset) GetRule(collectionID string) *Rule {
+	return &Rule{
+		CollectionID:    collectionID,
+		Actions:         []Action{ActionRead, ActionCreate, ActionUpdate, ActionDelete},
+		HiddenFields:    []string{},
+		ReadonlyFields:  []string{},
+		WriteonlyFields: []string{},
+	}
+}
+
+func Create(r Ruleset, collectionID string) *Permission {
+	return r.Permission(collectionID, ActionCreate)
+}
+
+func Read(r Ruleset, collectionID string) *Permission {
+	return r.Permission(collectionID, ActionRead)
+}
+
+func Update(r Ruleset, collectionID string) *Permission {
+	return r.Permission(collectionID, ActionUpdate)
+}
+
+func Delete(r Ruleset, collectionID string) *Permission {
+	return r.Permission(collectionID, ActionDelete)
+}
diff --git a/pkg/permission/ruleset_test.go b/pkg/permission/ruleset_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..47c1f07614b1d2edef196db6238adaf40b33e073
--- /dev/null
+++ b/pkg/permission/ruleset_test.go
@@ -0,0 +1,51 @@
+package permission
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMerge(t *testing.T) {
+	const (
+		col1 = "colID"
+		col2 = "colViewID"
+	)
+
+	tests := []struct {
+		name                  string
+		first, second, expect *Rule
+	}{
+		{
+			name:   "simple",
+			first:  &Rule{Actions: []Action{ActionUpdate, ActionCreate}, CollectionID: col1, HiddenFields: []string{"1", "2"}, WriteonlyFields: []string{"7"}, ReadonlyFields: []string{"4"}, ReadFilter: "3 != 'test'", WriteFilter: "4 == '0_0'"},
+			second: &Rule{Actions: []Action{ActionUpdate, ActionDelete}, CollectionID: col2, HiddenFields: []string{"3", "2"}, WriteonlyFields: []string{}, ReadonlyFields: []string{"5"}, ReadFilter: "5 != 'dev'", WriteFilter: "4 == '0_0'"},
+			expect: &Rule{Actions: []Action{ActionUpdate}, HiddenFields: []string{"1", "2", "3"}, WriteonlyFields: []string{"7"}, ReadonlyFields: []string{"4", "5"}, ReadFilter: "3 != 'test' && 5 != 'dev'", WriteFilter: "4 == '0_0'"},
+		},
+		{
+			name:   "first is privileged",
+			first:  PrivilegedRuleset{}.GetRule(col1),
+			second: &Rule{Actions: []Action{ActionUpdate, ActionDelete}, CollectionID: col2, WriteFilter: "test"},
+			expect: &Rule{Actions: []Action{ActionUpdate, ActionDelete}, WriteFilter: "test", ReadFilter: "", HiddenFields: []string{}, WriteonlyFields: []string{}, ReadonlyFields: []string{}},
+		},
+		{
+			name:   "second is privileged",
+			first:  &Rule{Actions: []Action{ActionUpdate, ActionDelete}, CollectionID: col1, WriteFilter: "test"},
+			second: PrivilegedRuleset{}.GetRule(col2),
+			expect: &Rule{Actions: []Action{ActionUpdate, ActionDelete}, WriteFilter: "test", ReadFilter: "", HiddenFields: []string{}, WriteonlyFields: []string{}, ReadonlyFields: []string{}},
+		},
+		{
+			name:   "both is privileged",
+			first:  PrivilegedRuleset{}.GetRule(col1),
+			second: PrivilegedRuleset{}.GetRule(col2),
+			expect: PrivilegedRuleset{}.GetRule(""),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := MergeRule(tt.first, tt.second)
+			assert.Equal(t, tt.expect, result)
+		})
+	}
+}
diff --git a/pkg/space/mocks/Spaces.go b/pkg/spaces/mocks/Spaces.go
similarity index 70%
rename from pkg/space/mocks/Spaces.go
rename to pkg/spaces/mocks/Spaces.go
index 6fd9ef4aaf8012c82973ed3d94815babed9acd65..e272ebceb87c26a672a3aaf5f7dde3dd4858991e 100644
--- a/pkg/space/mocks/Spaces.go
+++ b/pkg/spaces/mocks/Spaces.go
@@ -5,7 +5,7 @@ package mocks
 import (
 	context "context"
 
-	space "git.perx.ru/perxis/perxis-go/pkg/space"
+	spaces "git.perx.ru/perxis/perxis-go/pkg/spaces"
 	mock "github.com/stretchr/testify/mock"
 )
 
@@ -15,23 +15,23 @@ type Spaces struct {
 }
 
 // Create provides a mock function with given fields: ctx, _a1
-func (_m *Spaces) Create(ctx context.Context, _a1 *space.Space) (*space.Space, error) {
+func (_m *Spaces) Create(ctx context.Context, _a1 *spaces.Space) (*spaces.Space, error) {
 	ret := _m.Called(ctx, _a1)
 
-	var r0 *space.Space
+	var r0 *spaces.Space
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *space.Space) (*space.Space, error)); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Space) (*spaces.Space, error)); ok {
 		return rf(ctx, _a1)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *space.Space) *space.Space); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Space) *spaces.Space); ok {
 		r0 = rf(ctx, _a1)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*space.Space)
+			r0 = ret.Get(0).(*spaces.Space)
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *space.Space) error); ok {
+	if rf, ok := ret.Get(1).(func(context.Context, *spaces.Space) error); ok {
 		r1 = rf(ctx, _a1)
 	} else {
 		r1 = ret.Error(1)
@@ -55,19 +55,19 @@ func (_m *Spaces) Delete(ctx context.Context, spaceId string) error {
 }
 
 // Get provides a mock function with given fields: ctx, spaceId
-func (_m *Spaces) Get(ctx context.Context, spaceId string) (*space.Space, error) {
+func (_m *Spaces) Get(ctx context.Context, spaceId string) (*spaces.Space, error) {
 	ret := _m.Called(ctx, spaceId)
 
-	var r0 *space.Space
+	var r0 *spaces.Space
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, string) (*space.Space, error)); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, string) (*spaces.Space, error)); ok {
 		return rf(ctx, spaceId)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, string) *space.Space); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, string) *spaces.Space); ok {
 		r0 = rf(ctx, spaceId)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(*space.Space)
+			r0 = ret.Get(0).(*spaces.Space)
 		}
 	}
 
@@ -81,19 +81,19 @@ func (_m *Spaces) Get(ctx context.Context, spaceId string) (*space.Space, error)
 }
 
 // List provides a mock function with given fields: ctx, orgId
-func (_m *Spaces) List(ctx context.Context, orgId string) ([]*space.Space, error) {
+func (_m *Spaces) List(ctx context.Context, orgId string) ([]*spaces.Space, error) {
 	ret := _m.Called(ctx, orgId)
 
-	var r0 []*space.Space
+	var r0 []*spaces.Space
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, string) ([]*space.Space, error)); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, string) ([]*spaces.Space, error)); ok {
 		return rf(ctx, orgId)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, string) []*space.Space); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*spaces.Space); ok {
 		r0 = rf(ctx, orgId)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]*space.Space)
+			r0 = ret.Get(0).([]*spaces.Space)
 		}
 	}
 
@@ -107,11 +107,11 @@ func (_m *Spaces) List(ctx context.Context, orgId string) ([]*space.Space, error
 }
 
 // Update provides a mock function with given fields: ctx, _a1
-func (_m *Spaces) Update(ctx context.Context, _a1 *space.Space) error {
+func (_m *Spaces) Update(ctx context.Context, _a1 *spaces.Space) error {
 	ret := _m.Called(ctx, _a1)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, *space.Space) error); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Space) error); ok {
 		r0 = rf(ctx, _a1)
 	} else {
 		r0 = ret.Error(0)
@@ -121,11 +121,11 @@ func (_m *Spaces) Update(ctx context.Context, _a1 *space.Space) error {
 }
 
 // UpdateConfig provides a mock function with given fields: ctx, spaceId, config
-func (_m *Spaces) UpdateConfig(ctx context.Context, spaceId string, config *space.Config) error {
+func (_m *Spaces) UpdateConfig(ctx context.Context, spaceId string, config *spaces.Config) error {
 	ret := _m.Called(ctx, spaceId, config)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, string, *space.Config) error); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, string, *spaces.Config) error); ok {
 		r0 = rf(ctx, spaceId, config)
 	} else {
 		r0 = ret.Error(0)
diff --git a/pkg/space/service.go b/pkg/spaces/service.go
similarity index 99%
rename from pkg/space/service.go
rename to pkg/spaces/service.go
index 576f5c2f8a7fae3a2b15d658eb1f42b5df0cab51..d31cbfebec963d6b48ee618bfc7af85236b6aed3 100644
--- a/pkg/space/service.go
+++ b/pkg/spaces/service.go
@@ -1,4 +1,4 @@
-package space
+package spaces
 
 import (
 	"context"
diff --git a/pkg/space/space.go b/pkg/spaces/space.go
similarity index 99%
rename from pkg/space/space.go
rename to pkg/spaces/space.go
index dad51255a6fa9e8e648eec346a92f45c1749c876..772caa9cb248d555551de5096eb75c6ebd5e66a4 100644
--- a/pkg/space/space.go
+++ b/pkg/spaces/space.go
@@ -1,4 +1,4 @@
-package space
+package spaces
 
 type State int
 
diff --git a/pkg/space/transport/client.go b/pkg/spaces/transport/client.go
similarity index 97%
rename from pkg/space/transport/client.go
rename to pkg/spaces/transport/client.go
index 44b72b6f5c8f0786f7ad7a255b9e46b69f672647..4f13271e628dba08e0806e462dfed21e0f1bb7ec 100644
--- a/pkg/space/transport/client.go
+++ b/pkg/spaces/transport/client.go
@@ -5,7 +5,7 @@ package transport
 import (
 	"context"
 
-	spaces "git.perx.ru/perxis/perxis-go/pkg/space"
+	spaces "git.perx.ru/perxis/perxis-go/pkg/spaces"
 )
 
 func (set EndpointsSet) Create(arg0 context.Context, arg1 *spaces.Space) (res0 *spaces.Space, res1 error) {
diff --git a/pkg/space/transport/endpoints.microgen.go b/pkg/spaces/transport/endpoints.microgen.go
similarity index 100%
rename from pkg/space/transport/endpoints.microgen.go
rename to pkg/spaces/transport/endpoints.microgen.go
diff --git a/pkg/space/transport/exchanges.microgen.go b/pkg/spaces/transport/exchanges.microgen.go
similarity index 94%
rename from pkg/space/transport/exchanges.microgen.go
rename to pkg/spaces/transport/exchanges.microgen.go
index 05ad11827f34070ebd644b44a217bcceedfecbc7..106753c63bd80f38c68048767ef83c00fe19a8a4 100644
--- a/pkg/space/transport/exchanges.microgen.go
+++ b/pkg/spaces/transport/exchanges.microgen.go
@@ -2,7 +2,7 @@
 
 package transport
 
-import spaces "git.perx.ru/perxis/perxis-go/pkg/space"
+import spaces "git.perx.ru/perxis/perxis-go/pkg/spaces"
 
 type (
 	CreateRequest struct {
diff --git a/pkg/space/transport/grpc/client.go b/pkg/spaces/transport/grpc/client.go
similarity index 92%
rename from pkg/space/transport/grpc/client.go
rename to pkg/spaces/transport/grpc/client.go
index bfa48ae9df7c4bff443f5bbaa3d88c37f1fa26bc..693011ce92883ad758d1764ec6b2b6e34bc1782b 100644
--- a/pkg/space/transport/grpc/client.go
+++ b/pkg/spaces/transport/grpc/client.go
@@ -4,7 +4,7 @@ package transportgrpc
 
 import (
 	grpcerr "git.perx.ru/perxis/perxis-go/pkg/errors/grpc"
-	transport "git.perx.ru/perxis/perxis-go/pkg/space/transport"
+	transport "git.perx.ru/perxis/perxis-go/pkg/spaces/transport"
 	grpckit "github.com/go-kit/kit/transport/grpc"
 	grpc "google.golang.org/grpc"
 )
diff --git a/pkg/space/transport/grpc/client.microgen.go b/pkg/spaces/transport/grpc/client.microgen.go
similarity index 95%
rename from pkg/space/transport/grpc/client.microgen.go
rename to pkg/spaces/transport/grpc/client.microgen.go
index b0d7d35e8e14c5ffa1d852d16e899160af5bb55e..d6547f84c58f89e44f84e2e92aa325ca349a8919 100644
--- a/pkg/space/transport/grpc/client.microgen.go
+++ b/pkg/spaces/transport/grpc/client.microgen.go
@@ -3,7 +3,7 @@
 package transportgrpc
 
 import (
-	transport "git.perx.ru/perxis/perxis-go/pkg/space/transport"
+	transport "git.perx.ru/perxis/perxis-go/pkg/spaces/transport"
 	pb "git.perx.ru/perxis/perxis-go/proto/spaces"
 	grpckit "github.com/go-kit/kit/transport/grpc"
 	empty "github.com/golang/protobuf/ptypes/empty"
diff --git a/pkg/space/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/spaces/transport/grpc/protobuf_endpoint_converters.microgen.go
similarity index 99%
rename from pkg/space/transport/grpc/protobuf_endpoint_converters.microgen.go
rename to pkg/spaces/transport/grpc/protobuf_endpoint_converters.microgen.go
index 606d41cfa3335674b487cd861fd5cfb86152b90b..3ea51a2a40ae3b6fbe2a8ef4be0fbae4cf0620f3 100644
--- a/pkg/space/transport/grpc/protobuf_endpoint_converters.microgen.go
+++ b/pkg/spaces/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -7,7 +7,7 @@ import (
 	"context"
 	"errors"
 
-	transport "git.perx.ru/perxis/perxis-go/pkg/space/transport"
+	transport "git.perx.ru/perxis/perxis-go/pkg/spaces/transport"
 	pb "git.perx.ru/perxis/perxis-go/proto/spaces"
 	empty "github.com/golang/protobuf/ptypes/empty"
 )
diff --git a/pkg/space/transport/grpc/protobuf_type_converters.microgen.go b/pkg/spaces/transport/grpc/protobuf_type_converters.microgen.go
similarity index 97%
rename from pkg/space/transport/grpc/protobuf_type_converters.microgen.go
rename to pkg/spaces/transport/grpc/protobuf_type_converters.microgen.go
index 56e2dc38265de3a9fa239b3d4d8cb37873558796..27bb510db48d5d8ac09ad6101989a2395a47a32a 100644
--- a/pkg/space/transport/grpc/protobuf_type_converters.microgen.go
+++ b/pkg/spaces/transport/grpc/protobuf_type_converters.microgen.go
@@ -5,7 +5,7 @@
 package transportgrpc
 
 import (
-	service "git.perx.ru/perxis/perxis-go/pkg/space"
+	service "git.perx.ru/perxis/perxis-go/pkg/spaces"
 	pb "git.perx.ru/perxis/perxis-go/proto/spaces"
 )
 
diff --git a/pkg/space/transport/grpc/server.go b/pkg/spaces/transport/grpc/server.go
similarity index 80%
rename from pkg/space/transport/grpc/server.go
rename to pkg/spaces/transport/grpc/server.go
index f8220070b19fe5cc9a51ed7bdd8aa6fe5579d428..bd38b0b922277ec39816503413faa840535d6617 100644
--- a/pkg/space/transport/grpc/server.go
+++ b/pkg/spaces/transport/grpc/server.go
@@ -2,13 +2,13 @@ package transportgrpc
 
 import (
 	grpcerr "git.perx.ru/perxis/perxis-go/pkg/errors/grpc"
-	"git.perx.ru/perxis/perxis-go/pkg/space"
-	"git.perx.ru/perxis/perxis-go/pkg/space/transport"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces/transport"
 	pb "git.perx.ru/perxis/perxis-go/proto/spaces"
 	grpckit "github.com/go-kit/kit/transport/grpc"
 )
 
-func NewServer(svc space.Spaces, opts ...grpckit.ServerOption) pb.SpacesServer {
+func NewServer(svc spaces.Spaces, opts ...grpckit.ServerOption) pb.SpacesServer {
 	eps := transport.Endpoints(svc)
 	eps = transport.EndpointsSet{
 		CreateEndpoint:       grpcerr.ServerMiddleware(eps.CreateEndpoint),
diff --git a/pkg/space/transport/grpc/server.microgen.go b/pkg/spaces/transport/grpc/server.microgen.go
similarity index 97%
rename from pkg/space/transport/grpc/server.microgen.go
rename to pkg/spaces/transport/grpc/server.microgen.go
index 0d00433457f573073eeeabe5e41c4a3803d69c20..437b8ad385dfcb8c912c356d2ac41506ea49de79 100644
--- a/pkg/space/transport/grpc/server.microgen.go
+++ b/pkg/spaces/transport/grpc/server.microgen.go
@@ -4,7 +4,7 @@
 package transportgrpc
 
 import (
-	transport "git.perx.ru/perxis/perxis-go/pkg/space/transport"
+	transport "git.perx.ru/perxis/perxis-go/pkg/spaces/transport"
 	pb "git.perx.ru/perxis/perxis-go/proto/spaces"
 	grpc "github.com/go-kit/kit/transport/grpc"
 	empty "github.com/golang/protobuf/ptypes/empty"
diff --git a/pkg/space/transport/server.microgen.go b/pkg/spaces/transport/server.microgen.go
similarity index 97%
rename from pkg/space/transport/server.microgen.go
rename to pkg/spaces/transport/server.microgen.go
index b4546383c62dae4fdc994a79f17ba3ee4345ceb1..864dbffe26d8422848f44c6f27b513ae5033a521 100644
--- a/pkg/space/transport/server.microgen.go
+++ b/pkg/spaces/transport/server.microgen.go
@@ -5,7 +5,7 @@ package transport
 import (
 	"context"
 
-	spaces "git.perx.ru/perxis/perxis-go/pkg/space"
+	spaces "git.perx.ru/perxis/perxis-go/pkg/spaces"
 	endpoint "github.com/go-kit/kit/endpoint"
 )
 
diff --git a/pkg/users/mocks/Users.go b/pkg/users/mocks/Users.go
new file mode 100644
index 0000000000000000000000000000000000000000..6e54f18c4dfc78d8473e5499262eb7d65c783e4d
--- /dev/null
+++ b/pkg/users/mocks/Users.go
@@ -0,0 +1,143 @@
+// Code generated by mockery v2.7.4. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	users "git.perx.ru/perxis/perxis-go/pkg/users"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Users is an autogenerated mock type for the Users type
+type Users struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, create
+func (_m *Users) Create(ctx context.Context, create *users.User) (*users.User, error) {
+	ret := _m.Called(ctx, create)
+
+	var r0 *users.User
+	if rf, ok := ret.Get(0).(func(context.Context, *users.User) *users.User); ok {
+		r0 = rf(ctx, create)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*users.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *users.User) error); ok {
+		r1 = rf(ctx, create)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, userId
+func (_m *Users) Delete(ctx context.Context, userId string) error {
+	ret := _m.Called(ctx, userId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, userId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Find provides a mock function with given fields: ctx, filter, options
+func (_m *Users) Find(ctx context.Context, filter *users.Filter, opts *options.FindOptions) ([]*users.User, int, error) {
+	ret := _m.Called(ctx, filter, opts)
+
+	var r0 []*users.User
+	if rf, ok := ret.Get(0).(func(context.Context, *users.Filter, *options.FindOptions) []*users.User); ok {
+		r0 = rf(ctx, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*users.User)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *users.Filter, *options.FindOptions) int); ok {
+		r1 = rf(ctx, filter, opts)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *users.Filter, *options.FindOptions) error); ok {
+		r2 = rf(ctx, filter, opts)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// Get provides a mock function with given fields: ctx, userId
+func (_m *Users) Get(ctx context.Context, userId string) (*users.User, error) {
+	ret := _m.Called(ctx, userId)
+
+	var r0 *users.User
+	if rf, ok := ret.Get(0).(func(context.Context, string) *users.User); ok {
+		r0 = rf(ctx, userId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*users.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, userId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetByIdentity provides a mock function with given fields: ctx, identity
+func (_m *Users) GetByIdentity(ctx context.Context, identity string) (*users.User, error) {
+	ret := _m.Called(ctx, identity)
+
+	var r0 *users.User
+	if rf, ok := ret.Get(0).(func(context.Context, string) *users.User); ok {
+		r0 = rf(ctx, identity)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*users.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, identity)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Update provides a mock function with given fields: ctx, update
+func (_m *Users) Update(ctx context.Context, update *users.User) error {
+	ret := _m.Called(ctx, update)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *users.User) error); ok {
+		r0 = rf(ctx, update)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
diff --git a/pkg/users/service.go b/pkg/users/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..d64a5ceceed51d3731eaa6641baf64409cc7389d
--- /dev/null
+++ b/pkg/users/service.go
@@ -0,0 +1,30 @@
+package users
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+// @microgen grpc
+// @protobuf git.perx.ru/perxis/perxis-go/proto/users
+// @grpc-addr account.users.Users
+type Users interface {
+	Create(ctx context.Context, create *User) (user *User, err error)
+	Get(ctx context.Context, userId string) (user *User, err error)
+	Find(ctx context.Context, filter *Filter, options *options.FindOptions) (users []*User, total int, err error)
+	Update(ctx context.Context, update *User) (err error)
+	Delete(ctx context.Context, userId string) (err error)
+	GetByIdentity(ctx context.Context, identity string) (user *User, err error)
+}
+
+type Filter struct {
+	ID            []string
+	Name          []string
+	Identities    []string
+	DisplayName   []string
+	Email         []string
+	AvatarUri     []string
+	EmailVerified *bool
+	System        *bool
+}
diff --git a/pkg/users/transport/client.microgen.go b/pkg/users/transport/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..74ca261a3bc5ec1cf99c655c82ec5b0345489ce3
--- /dev/null
+++ b/pkg/users/transport/client.microgen.go
@@ -0,0 +1,88 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	"errors"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	users "git.perx.ru/perxis/perxis-go/pkg/users"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+func (set EndpointsSet) Create(arg0 context.Context, arg1 *users.User) (res0 *users.User, res1 error) {
+	request := CreateRequest{Create: arg1}
+	response, res1 := set.CreateEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*CreateResponse).User, res1
+}
+
+func (set EndpointsSet) Get(arg0 context.Context, arg1 string) (res0 *users.User, res1 error) {
+	request := GetRequest{UserId: arg1}
+	response, res1 := set.GetEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*GetResponse).User, res1
+}
+
+func (set EndpointsSet) Find(arg0 context.Context, arg1 *users.Filter, arg2 *options.FindOptions) (res0 []*users.User, res1 int, res2 error) {
+	request := FindRequest{
+		Filter:  arg1,
+		Options: arg2,
+	}
+	response, res2 := set.FindEndpoint(arg0, &request)
+	if res2 != nil {
+		if e, ok := status.FromError(res2); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res2 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*FindResponse).Users, response.(*FindResponse).Total, res2
+}
+
+func (set EndpointsSet) Update(arg0 context.Context, arg1 *users.User) (res0 error) {
+	request := UpdateRequest{Update: arg1}
+	_, res0 = set.UpdateEndpoint(arg0, &request)
+	if res0 != nil {
+		if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res0 = errors.New(e.Message())
+		}
+		return
+	}
+	return res0
+}
+
+func (set EndpointsSet) Delete(arg0 context.Context, arg1 string) (res0 error) {
+	request := DeleteRequest{UserId: arg1}
+	_, res0 = set.DeleteEndpoint(arg0, &request)
+	if res0 != nil {
+		if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res0 = errors.New(e.Message())
+		}
+		return
+	}
+	return res0
+}
+
+func (set EndpointsSet) GetByIdentity(arg0 context.Context, arg1 string) (res0 *users.User, res1 error) {
+	request := GetByIdentityRequest{Identity: arg1}
+	response, res1 := set.GetByIdentityEndpoint(arg0, &request)
+	if res1 != nil {
+		if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown {
+			res1 = errors.New(e.Message())
+		}
+		return
+	}
+	return response.(*GetByIdentityResponse).User, res1
+}
diff --git a/pkg/users/transport/endpoints.microgen.go b/pkg/users/transport/endpoints.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..264025bfc25edd39b423f5ca983188bc6e3a9e60
--- /dev/null
+++ b/pkg/users/transport/endpoints.microgen.go
@@ -0,0 +1,15 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import endpoint "github.com/go-kit/kit/endpoint"
+
+// EndpointsSet implements Users API and used for transport purposes.
+type EndpointsSet struct {
+	CreateEndpoint        endpoint.Endpoint
+	GetEndpoint           endpoint.Endpoint
+	FindEndpoint          endpoint.Endpoint
+	UpdateEndpoint        endpoint.Endpoint
+	DeleteEndpoint        endpoint.Endpoint
+	GetByIdentityEndpoint endpoint.Endpoint
+}
diff --git a/pkg/users/transport/exchanges.microgen.go b/pkg/users/transport/exchanges.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..f70b8cdfe93eb8b843ec66136e8fa63bccc735ee
--- /dev/null
+++ b/pkg/users/transport/exchanges.microgen.go
@@ -0,0 +1,52 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	users "git.perx.ru/perxis/perxis-go/pkg/users"
+)
+
+type (
+	CreateRequest struct {
+		Create *users.User `json:"create"`
+	}
+	CreateResponse struct {
+		User *users.User `json:"user"`
+	}
+
+	GetRequest struct {
+		UserId string `json:"user_id"`
+	}
+	GetResponse struct {
+		User *users.User `json:"user"`
+	}
+
+	FindRequest struct {
+		Filter  *users.Filter        `json:"filter"`
+		Options *options.FindOptions `json:"options"`
+	}
+	FindResponse struct {
+		Users []*users.User `json:"users"`
+		Total int           `json:"total"`
+	}
+
+	UpdateRequest struct {
+		Update *users.User `json:"update"`
+	}
+	// Formal exchange type, please do not delete.
+	UpdateResponse struct{}
+
+	DeleteRequest struct {
+		UserId string `json:"user_id"`
+	}
+	// Formal exchange type, please do not delete.
+	DeleteResponse struct{}
+
+	GetByIdentityRequest struct {
+		Identity string `json:"identity"`
+	}
+	GetByIdentityResponse struct {
+		User *users.User `json:"user"`
+	}
+)
diff --git a/pkg/users/transport/grpc/client.microgen.go b/pkg/users/transport/grpc/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..98f4b74c1d23e667d8c1d2c7290cd136f5e20a5d
--- /dev/null
+++ b/pkg/users/transport/grpc/client.microgen.go
@@ -0,0 +1,61 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/users/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/users"
+	grpckit "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	grpc "google.golang.org/grpc"
+)
+
+func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet {
+	if addr == "" {
+		addr = "account.users.Users"
+	}
+	return transport.EndpointsSet{
+		CreateEndpoint: grpckit.NewClient(
+			conn, addr, "Create",
+			_Encode_Create_Request,
+			_Decode_Create_Response,
+			pb.CreateResponse{},
+			opts...,
+		).Endpoint(),
+		DeleteEndpoint: grpckit.NewClient(
+			conn, addr, "Delete",
+			_Encode_Delete_Request,
+			_Decode_Delete_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+		FindEndpoint: grpckit.NewClient(
+			conn, addr, "Find",
+			_Encode_Find_Request,
+			_Decode_Find_Response,
+			pb.FindResponse{},
+			opts...,
+		).Endpoint(),
+		GetByIdentityEndpoint: grpckit.NewClient(
+			conn, addr, "GetByIdentity",
+			_Encode_GetByIdentity_Request,
+			_Decode_GetByIdentity_Response,
+			pb.GetByIdentityResponse{},
+			opts...,
+		).Endpoint(),
+		GetEndpoint: grpckit.NewClient(
+			conn, addr, "Get",
+			_Encode_Get_Request,
+			_Decode_Get_Response,
+			pb.GetResponse{},
+			opts...,
+		).Endpoint(),
+		UpdateEndpoint: grpckit.NewClient(
+			conn, addr, "Update",
+			_Encode_Update_Request,
+			_Decode_Update_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+	}
+}
diff --git a/pkg/users/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/users/transport/grpc/protobuf_endpoint_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..1837d41081572ab74ebfd7c9594a8292b4c9de29
--- /dev/null
+++ b/pkg/users/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -0,0 +1,265 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// Please, do not change functions names!
+package transportgrpc
+
+import (
+	"context"
+	"errors"
+
+	transport "git.perx.ru/perxis/perxis-go/pkg/users/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/users"
+	empty "github.com/golang/protobuf/ptypes/empty"
+)
+
+func _Encode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil CreateRequest")
+	}
+	req := request.(*transport.CreateRequest)
+	reqCreate, err := PtrUserToProto(req.Create)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateRequest{Create: reqCreate}, nil
+}
+
+func _Encode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetRequest")
+	}
+	req := request.(*transport.GetRequest)
+	return &pb.GetRequest{UserId: req.UserId}, nil
+}
+
+func _Encode_Find_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil FindRequest")
+	}
+	req := request.(*transport.FindRequest)
+	reqFilter, err := PtrFilterToProto(req.Filter)
+	if err != nil {
+		return nil, err
+	}
+	reqOptions, err := PtrServicesFindOptionsToProto(req.Options)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.FindRequest{
+		Filter:  reqFilter,
+		Options: reqOptions,
+	}, nil
+}
+
+func _Encode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil UpdateRequest")
+	}
+	req := request.(*transport.UpdateRequest)
+	reqUpdate, err := PtrUserToProto(req.Update)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.UpdateRequest{Update: reqUpdate}, nil
+}
+
+func _Encode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil DeleteRequest")
+	}
+	req := request.(*transport.DeleteRequest)
+	return &pb.DeleteRequest{UserId: req.UserId}, nil
+}
+
+func _Encode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil CreateResponse")
+	}
+	resp := response.(*transport.CreateResponse)
+	respUser, err := PtrUserToProto(resp.User)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateResponse{User: respUser}, nil
+}
+
+func _Encode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetResponse")
+	}
+	resp := response.(*transport.GetResponse)
+	respUser, err := PtrUserToProto(resp.User)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.GetResponse{User: respUser}, nil
+}
+
+func _Encode_Find_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil FindResponse")
+	}
+	resp := response.(*transport.FindResponse)
+	respUsers, err := ListPtrUserToProto(resp.Users)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.FindResponse{
+		Total: int64(resp.Total),
+		Users: respUsers,
+	}, nil
+}
+
+func _Encode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil CreateRequest")
+	}
+	req := request.(*pb.CreateRequest)
+	reqCreate, err := ProtoToPtrUser(req.Create)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateRequest{Create: reqCreate}, nil
+}
+
+func _Decode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetRequest")
+	}
+	req := request.(*pb.GetRequest)
+	return &transport.GetRequest{UserId: string(req.UserId)}, nil
+}
+
+func _Decode_Find_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil FindRequest")
+	}
+	req := request.(*pb.FindRequest)
+	reqFilter, err := ProtoToPtrFilter(req.Filter)
+	if err != nil {
+		return nil, err
+	}
+	reqOptions, err := ProtoToPtrServicesFindOptions(req.Options)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.FindRequest{
+		Filter:  reqFilter,
+		Options: reqOptions,
+	}, nil
+}
+
+func _Decode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil UpdateRequest")
+	}
+	req := request.(*pb.UpdateRequest)
+	reqUpdate, err := ProtoToPtrUser(req.Update)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.UpdateRequest{Update: reqUpdate}, nil
+}
+
+func _Decode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil DeleteRequest")
+	}
+	req := request.(*pb.DeleteRequest)
+	return &transport.DeleteRequest{UserId: string(req.UserId)}, nil
+}
+
+func _Decode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil CreateResponse")
+	}
+	resp := response.(*pb.CreateResponse)
+	respUser, err := ProtoToPtrUser(resp.User)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateResponse{User: respUser}, nil
+}
+
+func _Decode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetResponse")
+	}
+	resp := response.(*pb.GetResponse)
+	respUser, err := ProtoToPtrUser(resp.User)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.GetResponse{User: respUser}, nil
+}
+
+func _Decode_Find_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil FindResponse")
+	}
+	resp := response.(*pb.FindResponse)
+	respUsers, err := ProtoToListPtrUser(resp.Users)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.FindResponse{
+		Total: int(resp.Total),
+		Users: respUsers,
+	}, nil
+}
+
+func _Decode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Encode_GetByIdentity_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetByIdentityRequest")
+	}
+	req := request.(*transport.GetByIdentityRequest)
+	return &pb.GetByIdentityRequest{Identity: req.Identity}, nil
+}
+
+func _Encode_GetByIdentity_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetByIdentityResponse")
+	}
+	resp := response.(*transport.GetByIdentityResponse)
+	respUser, err := PtrUserToProto(resp.User)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.GetByIdentityResponse{User: respUser}, nil
+}
+
+func _Decode_GetByIdentity_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetByIdentityRequest")
+	}
+	req := request.(*pb.GetByIdentityRequest)
+	return &transport.GetByIdentityRequest{Identity: string(req.Identity)}, nil
+}
+
+func _Decode_GetByIdentity_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil GetByIdentityResponse")
+	}
+	resp := response.(*pb.GetByIdentityResponse)
+	respUser, err := ProtoToPtrUser(resp.User)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.GetByIdentityResponse{User: respUser}, nil
+}
+
+func _Encode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
diff --git a/pkg/users/transport/grpc/protobuf_type_converters.microgen.go b/pkg/users/transport/grpc/protobuf_type_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..d0789e52420b5383530e10f89e06100197a32bbb
--- /dev/null
+++ b/pkg/users/transport/grpc/protobuf_type_converters.microgen.go
@@ -0,0 +1,153 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// It is better for you if you do not change functions names!
+// This file will never be overwritten.
+package transportgrpc
+
+import (
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	service "git.perx.ru/perxis/perxis-go/pkg/users"
+	common "git.perx.ru/perxis/perxis-go/proto/common"
+	pb "git.perx.ru/perxis/perxis-go/proto/users"
+	"github.com/golang/protobuf/ptypes/wrappers"
+)
+
+func PtrUserToProto(create *service.User) (*pb.User, error) {
+	if create == nil {
+		return nil, nil
+	}
+	u := &pb.User{
+		Id:          create.ID,
+		Name:        create.Name,
+		Identities:  create.Identities,
+		DisplayName: create.DisplayName,
+		Email:       create.Email,
+		AvatarUrl:   create.AvatarURL,
+	}
+	if create.EmailVerified != nil {
+		u.EmailVerified = &wrappers.BoolValue{
+			Value: *create.EmailVerified,
+		}
+	}
+	if create.System != nil {
+		u.System = &wrappers.BoolValue{
+			Value: *create.System,
+		}
+	}
+	return u, nil
+}
+
+func ProtoToPtrUser(protoCreate *pb.User) (*service.User, error) {
+	if protoCreate == nil {
+		return nil, nil
+	}
+	user := &service.User{
+		ID:          protoCreate.Id,
+		Name:        protoCreate.Name,
+		DisplayName: protoCreate.DisplayName,
+		Identities:  protoCreate.Identities,
+		Email:       protoCreate.Email,
+		AvatarURL:   protoCreate.AvatarUrl,
+	}
+	if protoCreate.EmailVerified != nil {
+		user.EmailVerified = &protoCreate.EmailVerified.Value
+	}
+	if protoCreate.System != nil {
+		user.System = &protoCreate.System.Value
+	}
+	return user, nil
+}
+
+func PtrFilterToProto(filter *service.Filter) (*pb.Filter, error) {
+	if filter == nil {
+		return nil, nil
+	}
+	f := &pb.Filter{
+		Id:          filter.ID,
+		Name:        filter.Name,
+		Identities:  filter.Identities,
+		DisplayName: filter.DisplayName,
+		Email:       filter.Email,
+	}
+	if filter.EmailVerified != nil {
+		f.EmailVerified = &wrappers.BoolValue{
+			Value: *filter.EmailVerified,
+		}
+	}
+	if filter.System != nil {
+		f.System = &wrappers.BoolValue{
+			Value: *filter.System,
+		}
+	}
+	return f, nil
+}
+
+func ProtoToPtrFilter(protoFilter *pb.Filter) (*service.Filter, error) {
+	if protoFilter == nil {
+		return nil, nil
+	}
+	f := &service.Filter{
+		ID:          protoFilter.Id,
+		Name:        protoFilter.Name,
+		Identities:  protoFilter.Identities,
+		DisplayName: protoFilter.DisplayName,
+		Email:       protoFilter.Email,
+	}
+	if protoFilter.EmailVerified != nil {
+		f.EmailVerified = &protoFilter.EmailVerified.Value
+	}
+	if protoFilter.System != nil {
+		f.System = &protoFilter.System.Value
+	}
+	return f, nil
+}
+
+func ListPtrUserToProto(users []*service.User) ([]*pb.User, error) {
+	protoUsers := make([]*pb.User, 0, len(users))
+	for _, u := range users {
+		pu, err := PtrUserToProto(u)
+		if err != nil {
+			return nil, err
+		}
+		protoUsers = append(protoUsers, pu)
+	}
+	return protoUsers, nil
+}
+
+func ProtoToListPtrUser(protoCreates []*pb.User) ([]*service.User, error) {
+	users := make([]*service.User, 0, len(protoCreates))
+	for _, pu := range protoCreates {
+		u, err := ProtoToPtrUser(pu)
+		if err != nil {
+			return nil, err
+		}
+		users = append(users, u)
+	}
+	return users, nil
+}
+
+func PtrServicesFindOptionsToProto(opts *options.FindOptions) (*common.FindOptions, error) {
+	if opts == nil {
+		return nil, nil
+	}
+	return &common.FindOptions{
+		Sort:     opts.Sort,
+		PageNum:  int32(opts.PageNum),
+		PageSize: int32(opts.PageSize),
+	}, nil
+}
+
+func ProtoToPtrServicesFindOptions(protoOpts *common.FindOptions) (*options.FindOptions, error) {
+	if protoOpts == nil {
+		return nil, nil
+	}
+	return &options.FindOptions{
+		SortOptions: options.SortOptions{
+			Sort: protoOpts.Sort,
+		},
+		PaginationOptions: options.PaginationOptions{
+			PageNum:  int(protoOpts.PageNum),
+			PageSize: int(protoOpts.PageSize),
+		},
+	}, nil
+}
diff --git a/pkg/users/transport/grpc/server.microgen.go b/pkg/users/transport/grpc/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..2be01e97a5a98fcf5189b386fbb36d1a60f43db7
--- /dev/null
+++ b/pkg/users/transport/grpc/server.microgen.go
@@ -0,0 +1,112 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// DO NOT EDIT.
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/users/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/users"
+	grpc "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	context "golang.org/x/net/context"
+)
+
+type usersServer struct {
+	create        grpc.Handler
+	get           grpc.Handler
+	find          grpc.Handler
+	update        grpc.Handler
+	delete        grpc.Handler
+	getByIdentity grpc.Handler
+
+	pb.UnimplementedUsersServer
+}
+
+func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.UsersServer {
+	return &usersServer{
+		create: grpc.NewServer(
+			endpoints.CreateEndpoint,
+			_Decode_Create_Request,
+			_Encode_Create_Response,
+			opts...,
+		),
+		delete: grpc.NewServer(
+			endpoints.DeleteEndpoint,
+			_Decode_Delete_Request,
+			_Encode_Delete_Response,
+			opts...,
+		),
+		find: grpc.NewServer(
+			endpoints.FindEndpoint,
+			_Decode_Find_Request,
+			_Encode_Find_Response,
+			opts...,
+		),
+		get: grpc.NewServer(
+			endpoints.GetEndpoint,
+			_Decode_Get_Request,
+			_Encode_Get_Response,
+			opts...,
+		),
+		getByIdentity: grpc.NewServer(
+			endpoints.GetByIdentityEndpoint,
+			_Decode_GetByIdentity_Request,
+			_Encode_GetByIdentity_Response,
+			opts...,
+		),
+		update: grpc.NewServer(
+			endpoints.UpdateEndpoint,
+			_Decode_Update_Request,
+			_Encode_Update_Response,
+			opts...,
+		),
+	}
+}
+
+func (S *usersServer) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) {
+	_, resp, err := S.create.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.CreateResponse), nil
+}
+
+func (S *usersServer) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) {
+	_, resp, err := S.get.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.GetResponse), nil
+}
+
+func (S *usersServer) Find(ctx context.Context, req *pb.FindRequest) (*pb.FindResponse, error) {
+	_, resp, err := S.find.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.FindResponse), nil
+}
+
+func (S *usersServer) Update(ctx context.Context, req *pb.UpdateRequest) (*empty.Empty, error) {
+	_, resp, err := S.update.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
+
+func (S *usersServer) Delete(ctx context.Context, req *pb.DeleteRequest) (*empty.Empty, error) {
+	_, resp, err := S.delete.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
+
+func (S *usersServer) GetByIdentity(ctx context.Context, req *pb.GetByIdentityRequest) (*pb.GetByIdentityResponse, error) {
+	_, resp, err := S.getByIdentity.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.GetByIdentityResponse), nil
+}
diff --git a/pkg/users/transport/server.microgen.go b/pkg/users/transport/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..e12645efc79921da957e359848d9dca606f6364b
--- /dev/null
+++ b/pkg/users/transport/server.microgen.go
@@ -0,0 +1,72 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+
+	users "git.perx.ru/perxis/perxis-go/pkg/users"
+	endpoint "github.com/go-kit/kit/endpoint"
+)
+
+func Endpoints(svc users.Users) EndpointsSet {
+	return EndpointsSet{
+		CreateEndpoint:        CreateEndpoint(svc),
+		DeleteEndpoint:        DeleteEndpoint(svc),
+		FindEndpoint:          FindEndpoint(svc),
+		GetByIdentityEndpoint: GetByIdentityEndpoint(svc),
+		GetEndpoint:           GetEndpoint(svc),
+		UpdateEndpoint:        UpdateEndpoint(svc),
+	}
+}
+
+func CreateEndpoint(svc users.Users) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*CreateRequest)
+		res0, res1 := svc.Create(arg0, req.Create)
+		return &CreateResponse{User: res0}, res1
+	}
+}
+
+func GetEndpoint(svc users.Users) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetRequest)
+		res0, res1 := svc.Get(arg0, req.UserId)
+		return &GetResponse{User: res0}, res1
+	}
+}
+
+func FindEndpoint(svc users.Users) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*FindRequest)
+		res0, res1, res2 := svc.Find(arg0, req.Filter, req.Options)
+		return &FindResponse{
+			Total: res1,
+			Users: res0,
+		}, res2
+	}
+}
+
+func UpdateEndpoint(svc users.Users) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*UpdateRequest)
+		res0 := svc.Update(arg0, req.Update)
+		return &UpdateResponse{}, res0
+	}
+}
+
+func DeleteEndpoint(svc users.Users) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*DeleteRequest)
+		res0 := svc.Delete(arg0, req.UserId)
+		return &DeleteResponse{}, res0
+	}
+}
+
+func GetByIdentityEndpoint(svc users.Users) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetByIdentityRequest)
+		res0, res1 := svc.GetByIdentity(arg0, req.Identity)
+		return &GetByIdentityResponse{User: res0}, res1
+	}
+}
diff --git a/pkg/users/user.go b/pkg/users/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..9eca6efc24569c2ef951d2bcb059440d54896ecc
--- /dev/null
+++ b/pkg/users/user.go
@@ -0,0 +1,31 @@
+package users
+
+// Current - Идентификатор, который можно использовать для получения/обновления/регистрации
+// пользователя, от имени которого был сделан запрос.
+const Current = "current"
+
+type User struct {
+	ID            string   `json:"id" bson:"_id"`
+	Name          string   `json:"name" bson:"name"`
+	DisplayName   string   `json:"displayName" bson:"displayName"`
+	Identities    []string `json:"identities" bson:"identities"`
+	Email         string   `json:"email" bson:"email"`
+	EmailVerified *bool    `json:"emailVerified" bson:"emailVerified"`
+	AvatarURL     string   `json:"avatarUrl" bson:"avatarUrl,omitempty"`
+	System        *bool    `json:"system" bson:"system"`
+}
+
+func (u User) GetID() string {
+	return u.ID
+}
+
+func (u User) IsSystem() bool {
+	if u.System != nil {
+		return *u.System
+	}
+	return false
+}
+
+func (u User) Clone() *User {
+	return &u
+}