diff --git a/go.mod b/go.mod
index f53323bb290e119feda2d24ebe935d02cfdc9e38..14a85cd72aebd444fa384bc1788849052a937c4c 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,8 @@ require (
 	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/json-iterator/go v1.1.12
+	github.com/mitchellh/mapstructure v1.4.2
 	github.com/pkg/errors v0.9.1
 	github.com/rs/xid v1.4.0
 	github.com/stretchr/testify v1.8.0
@@ -25,13 +27,23 @@ require (
 	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/golang/snappy v0.0.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/klauspost/compress v1.13.6 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/stretchr/objx v0.4.0 // indirect
+	github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+	github.com/xdg-go/scram v1.1.1 // indirect
+	github.com/xdg-go/stringprep v1.0.3 // indirect
+	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.7.0 // indirect
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // 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
diff --git a/go.sum b/go.sum
index 9dea84ab8c222f13374d0a59a405fa290feed61a..511239a6e719ed8e4be2d67f6fc16c582aad70ef 100644
--- a/go.sum
+++ b/go.sum
@@ -57,6 +57,7 @@ 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 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 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=
@@ -67,6 +68,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 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=
@@ -79,6 +81,9 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
 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=
@@ -94,6 +99,14 @@ github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1
 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/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
+github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
 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=
@@ -122,9 +135,13 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
 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 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
 github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E=
 github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
+github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs=
 github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
 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=
@@ -170,6 +187,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
diff --git a/pkg/auth/anonymous.go b/pkg/auth/anonymous.go
new file mode 100644
index 0000000000000000000000000000000000000000..62aa8a1ac28c45f09a065906a7735e44c1add05e
--- /dev/null
+++ b/pkg/auth/anonymous.go
@@ -0,0 +1,127 @@
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+)
+
+type Anonymous struct {
+	roles        roles.Roles
+	spaces       spaces.Spaces
+	spaceID      string
+	environments environments.Environments
+}
+
+func (Anonymous) GetID(ctx context.Context) string  { return "anonymous" }
+func (Anonymous) IsValid(ctx context.Context) bool  { return false }
+func (Anonymous) IsSystem(ctx context.Context) bool { return false }
+func (Anonymous) IsManagementAllowed(ctx context.Context, spaceID string) error {
+	return ErrAccessDenied
+}
+
+func (a Anonymous) Space(spaceID string) SpaceAccessor {
+	a.spaceID = spaceID
+	return &a
+}
+
+func (a *Anonymous) getSpace(ctx context.Context, spaceID string) *spaces.Space {
+	if spaceID == "" {
+		return nil
+	}
+	space, _ := a.spaces.Get(WithSystem(ctx), spaceID)
+	return space
+}
+
+func (a *Anonymous) HasSpaceAccess(ctx context.Context, spaceID string) bool {
+	if a.spaceID == "" || a.spaces == nil {
+		return false
+	}
+	return a.Role(ctx, spaceID) != nil
+}
+
+func (a *Anonymous) Member(ctx context.Context) members.Role {
+	return members.NotMember
+}
+
+func (a *Anonymous) Role(ctx context.Context, spaceID string) *roles.Role {
+	if a.spaceID == "" || a.roles == nil {
+		return nil
+	}
+	role, err := a.roles.Get(WithSystem(ctx), spaceID, roles.AnonymousRole)
+	if err != nil {
+		return nil
+	}
+	return role
+}
+
+func (a *Anonymous) Rules(ctx context.Context, spaceID, envID string) permission.Ruleset {
+	role := a.Role(WithSystem(ctx), spaceID)
+	if role == nil {
+		return nil
+	}
+
+	if !a.HasEnvironmentAccess(ctx, spaceID, envID) {
+		return nil
+	}
+
+	return role.Rules
+}
+
+func (a *Anonymous) HasEnvironmentAccess(ctx context.Context, space, env string) bool {
+	return hasEnvironmentAccess(ctx, a.environments, a.Role(ctx, space), env)
+}
+
+func (Anonymous) Format(f fmt.State, verb rune) {
+	f.Write([]byte("AnonymousPrincipal{}"))
+}
+
+func (a Anonymous) HasAccess(ctx context.Context, spaceID, orgID string) error {
+	if !a.IsValid(ctx) {
+		return ErrAccessDenied
+	}
+
+	if a.IsSystem(ctx) {
+		return nil
+	}
+
+	if spaceID != "" {
+		hasAllow, err := a.hasRole(ctx, spaceID)
+		if err != nil {
+			return err
+		}
+
+		if hasAllow {
+			return nil
+		}
+	}
+
+	if a.Member(ctx).IsPrivileged() {
+		return nil
+	}
+
+	return ErrAccessDenied
+}
+
+func (a *Anonymous) hasRole(ctx context.Context, spaceID string) (bool, error) {
+	if a.spaceID == "" || a.roles == nil {
+		return false, nil
+	}
+	_, err := a.roles.Get(WithSystem(ctx), spaceID, roles.AnonymousRole)
+	if err == nil {
+		return true, nil
+	}
+
+	if errors.Is(err, ErrNotFound) {
+		if sp := a.getSpace(ctx, spaceID); sp == nil {
+			return false, ErrNotFound
+		}
+	}
+	return false, nil
+}
diff --git a/pkg/auth/client.go b/pkg/auth/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..cf63410f0c2efb4c3d2150ce3956a75c5e1cbfab
--- /dev/null
+++ b/pkg/auth/client.go
@@ -0,0 +1,255 @@
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+)
+
+type ClientPrincipal struct {
+	identity *clients.GetByParams
+	spaceID  string
+	space    *spaces.Space
+
+	client  *clients.Client
+	invalid bool
+
+	spaces        spaces.Spaces
+	environments  environments.Environments
+	clients       clients.Clients
+	roles         roles.Roles
+	collaborators collaborators.Collaborators
+}
+
+func NewClientPrincipal(identity *clients.GetByParams) *ClientPrincipal {
+	return &ClientPrincipal{identity: identity}
+}
+
+func (c ClientPrincipal) Format(f fmt.State, verb rune) {
+	var identity string
+	switch {
+	case c.identity == nil:
+		identity = "<nil>"
+	case c.identity.APIKey != "":
+		identity = fmt.Sprintf("APIKey: '%s'", c.identity.APIKey)
+	case c.identity.OAuthClientID != "":
+		identity = fmt.Sprintf("OAuthClientID: '%s'", c.identity.OAuthClientID)
+	case c.identity.TLSSubject != "":
+		identity = fmt.Sprintf("TLSSubject: '%s'", c.identity.TLSSubject)
+	}
+
+	var id string
+	if c.client != nil {
+		id = c.client.ID
+	}
+
+	f.Write([]byte(fmt.Sprintf("ClientPrincipal{ID: '%s', Identity: {%s}}", id, identity)))
+}
+
+func (c *ClientPrincipal) Space(spaceID string) SpaceAccessor {
+	c.spaceID = spaceID
+	c.space = nil
+	c.invalid = false
+	c.client = nil
+	return c
+}
+
+func (c *ClientPrincipal) getSpace(ctx context.Context, spaceID string) *spaces.Space {
+	if spaceID == "" {
+		return nil
+	}
+	space, _ := c.spaces.Get(WithSystem(ctx), spaceID)
+	return space
+}
+
+func (ClientPrincipal) IsSystem(ctx context.Context) bool {
+	return false
+}
+
+func (c *ClientPrincipal) IsManagementAllowed(ctx context.Context, spaceID string) error {
+	if !c.IsValid(ctx) {
+		return ErrAccessDenied
+	}
+
+	if role := c.Role(ctx, spaceID); role != nil && role.AllowManagement {
+		return nil
+	}
+
+	return ErrAccessDenied
+}
+
+func (c *ClientPrincipal) Member(ctx context.Context) members.Role {
+	return members.NotMember
+}
+
+func (c *ClientPrincipal) HasSpaceAccess(ctx context.Context, spaceID string) bool {
+	if c.spaceID == "" {
+		return false
+	}
+	client, _ := c.Client(ctx)
+	return client != nil && client.SpaceID == spaceID
+}
+
+func (c *ClientPrincipal) GetID(ctx context.Context) string {
+	client, _ := c.Client(ctx)
+	if client == nil {
+		return ""
+	}
+	return client.ID
+}
+
+func (c *ClientPrincipal) GetIdentity(ctx context.Context) *clients.GetByParams {
+	return c.identity
+}
+
+func (c *ClientPrincipal) IsValid(ctx context.Context) bool {
+	if c == nil {
+		return false
+	}
+	client, _ := c.Client(ctx)
+	return client != nil
+}
+
+func (c *ClientPrincipal) Client(ctx context.Context) (*clients.Client, error) {
+	if c.invalid {
+		return nil, nil
+	}
+
+	if c.client != nil {
+		return c.client, nil
+	}
+
+	if c.clients == nil {
+		c.invalid = true
+		return nil, nil
+	}
+
+	client, err := c.clients.GetBy(WithSystem(ctx), c.spaceID, c.identity)
+	if err != nil || client == nil || client.IsDisabled() {
+		c.invalid = true
+		return nil, err
+	}
+
+	c.client = client
+	return c.client, nil
+}
+
+func (c *ClientPrincipal) HasEnvironmentAccess(ctx context.Context, spaceID, envID string) bool {
+	return hasEnvironmentAccess(ctx, c.environments, c.Role(ctx, spaceID), envID)
+}
+
+func (c *ClientPrincipal) getRoleID(ctx context.Context, spaceID string) (string, bool) {
+
+	if c.spaceID == "" || spaceID == "" {
+		return "", false
+	}
+
+	if spaceID == c.spaceID {
+		cl, _ := c.Client(ctx)
+		if cl == nil || cl.RoleID == "" {
+			return "", false
+		}
+
+		return cl.RoleID, true
+	}
+
+	rID, err := c.collaborators.Get(WithSystem(ctx), spaceID, c.spaceID)
+	if err != nil {
+		rID = roles.ViewRole
+	}
+	return rID, true
+
+}
+
+func (c *ClientPrincipal) Role(ctx context.Context, spaceID string) *roles.Role {
+	if c.spaceID == "" {
+		return nil
+	}
+
+	rID, ok := c.getRoleID(ctx, spaceID)
+	if !ok {
+		return nil
+	}
+
+	role, err := c.roles.Get(WithSystem(ctx), spaceID, rID)
+	if err == nil {
+		//c.hasRole = true
+		//c.role = role
+		return role
+	}
+
+	return nil
+}
+
+func (c *ClientPrincipal) Rules(ctx context.Context, spaceID, envID string) permission.Ruleset {
+	if c.spaceID == "" || spaceID == "" || envID == "" {
+		return nil
+	}
+
+	role := c.Role(ctx, spaceID)
+	if role == nil {
+		return nil
+	}
+
+	if role.AllowManagement {
+		return permission.PrivilegedRuleset{}
+	}
+
+	if hasEnvironmentAccess(ctx, c.environments, role, envID) {
+		return role.Rules
+	}
+	return nil
+}
+
+func (c *ClientPrincipal) HasAccess(ctx context.Context, spaceID, orgID string) error {
+	if !c.IsValid(ctx) {
+		return ErrAccessDenied
+	}
+
+	if c.IsSystem(ctx) {
+		return nil
+	}
+
+	if spaceID != "" {
+		if c.spaceID == "" {
+			return ErrAccessDenied
+		}
+
+		client, _ := c.Client(ctx)
+		if client != nil && client.SpaceID == spaceID {
+			return nil
+		}
+	}
+
+	if c.Member(ctx).IsPrivileged() {
+		return nil
+	}
+
+	return ErrAccessDenied
+}
+
+func (c *ClientPrincipal) hasRole(ctx context.Context, spaceID string) (bool, error) {
+	if c.spaceID == "" {
+		return false, nil
+	}
+
+	client, err := c.Client(ctx)
+	if err != nil && errors.Is(err, ErrNotFound) {
+		if sp := c.getSpace(ctx, spaceID); sp == nil {
+			return false, ErrNotFound
+		}
+	}
+	if client != nil && client.SpaceID == spaceID {
+		return true, nil
+	}
+
+	return false, nil
+}
diff --git a/pkg/auth/context.go b/pkg/auth/context.go
new file mode 100644
index 0000000000000000000000000000000000000000..d447681068fefc974089f351d7654056314796b7
--- /dev/null
+++ b/pkg/auth/context.go
@@ -0,0 +1,27 @@
+package auth
+
+import (
+	"context"
+)
+
+type principalKey struct{}
+
+func GetPrincipal(ctx context.Context) Principal {
+	p, _ := ctx.Value(principalKey{}).(Principal)
+	if p == nil {
+		return Anonymous{}
+	}
+	return p
+}
+
+func WithPrincipal(ctx context.Context, p Principal) context.Context {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+
+	return context.WithValue(ctx, principalKey{}, p)
+}
+
+func WithSystem(ctx context.Context) context.Context {
+	return WithPrincipal(ctx, &SystemPrincipal{})
+}
diff --git a/pkg/auth/errors.go b/pkg/auth/errors.go
new file mode 100644
index 0000000000000000000000000000000000000000..8522ec94ff4ac7e04e6eabfdca561f698645db9d
--- /dev/null
+++ b/pkg/auth/errors.go
@@ -0,0 +1,10 @@
+package auth
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+)
+
+var (
+	ErrAccessDenied = errors.PermissionDenied(errors.New("access denied"))
+	ErrNotFound     = errors.NotFound(errors.New("not found"))
+)
diff --git a/pkg/auth/factory.go b/pkg/auth/factory.go
new file mode 100644
index 0000000000000000000000000000000000000000..2394c62a2f15ca7605959b3f5b31996a5c79a164
--- /dev/null
+++ b/pkg/auth/factory.go
@@ -0,0 +1,82 @@
+package auth
+
+import (
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+)
+
+type PrincipalFactory struct {
+	users.Users
+	members.Members
+	collaborators.Collaborators
+	roles.Roles
+	clients.Clients
+	spaces.Spaces
+	environments.Environments
+}
+
+func (f PrincipalFactory) User(identity string) Principal {
+	return &UserPrincipal{
+		identity:      identity,
+		users:         f.Users,
+		members:       f.Members,
+		roles:         f.Roles,
+		collaborators: f.Collaborators,
+		spaces:        f.Spaces,
+		environments:  f.Environments,
+	}
+}
+
+func (f PrincipalFactory) Client(param *clients.GetByParams) Principal {
+	return &ClientPrincipal{
+		identity: param,
+		//authID:       authID,
+		clients:       f.Clients,
+		environments:  f.Environments,
+		roles:         f.Roles,
+		spaces:        f.Spaces,
+		collaborators: f.Collaborators,
+	}
+}
+
+func (f PrincipalFactory) Anonymous() Principal {
+	return &Anonymous{
+		roles:  f.Roles,
+		spaces: f.Spaces,
+	}
+}
+
+func (f PrincipalFactory) System() Principal {
+	return &SystemPrincipal{}
+}
+
+func (f PrincipalFactory) Principal(principalId string) Principal {
+	switch {
+	case strings.Contains(principalId, "Subject="):
+		return f.Client(&clients.GetByParams{TLSSubject: getSubject(principalId)})
+	case strings.HasSuffix(principalId, "@clients"):
+		return f.Client(&clients.GetByParams{OAuthClientID: strings.TrimSuffix(principalId, "@clients")})
+	case strings.HasPrefix(principalId, "API-Key"):
+		return f.Client(&clients.GetByParams{APIKey: strings.TrimPrefix(principalId, "API-Key ")})
+	default:
+		return f.User(principalId)
+	}
+}
+
+func getSubject(header string) string {
+	var p string
+	for _, part := range strings.Split(header, ";") {
+		if strings.Contains(part, "Subject") {
+			p = strings.TrimSuffix(strings.TrimPrefix(part, "Subject=\""), "\"")
+			break
+		}
+	}
+	return p
+}
diff --git a/pkg/auth/grpc.go b/pkg/auth/grpc.go
new file mode 100644
index 0000000000000000000000000000000000000000..7a566711db76c11b22206d947ba94ecc2bc3b366
--- /dev/null
+++ b/pkg/auth/grpc.go
@@ -0,0 +1,92 @@
+package auth
+
+import (
+	"context"
+
+	kitgrpc "github.com/go-kit/kit/transport/grpc"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/metadata"
+)
+
+const (
+	OAuth2IdentityMetadata = "x-perxis-identity"
+	TLSIdentityMetadata    = "x-forwarded-client-cert"
+	AccessMetadata         = "x-perxis-access"
+
+	AuthorizationMetadata = "authorization"
+)
+
+func GRPCToContext(factory *PrincipalFactory) kitgrpc.ServerRequestFunc {
+	return func(ctx context.Context, md metadata.MD) context.Context {
+		if identity := md.Get(TLSIdentityMetadata); len(identity) > 0 {
+			return WithPrincipal(ctx, factory.Principal(identity[0]))
+		}
+
+		if identity := md.Get(OAuth2IdentityMetadata); len(identity) > 0 {
+			return WithPrincipal(ctx, factory.Principal(identity[0]))
+		}
+
+		if identity := md.Get(AuthorizationMetadata); len(identity) > 0 {
+			return WithPrincipal(ctx, factory.Principal(identity[0]))
+		}
+
+		if access := md.Get(AccessMetadata); len(access) > 0 {
+			return WithPrincipal(ctx, factory.System())
+		}
+
+		return WithPrincipal(ctx, factory.Anonymous())
+	}
+}
+
+func ContextToGRPC() kitgrpc.ClientRequestFunc {
+	return func(ctx context.Context, md *metadata.MD) context.Context {
+		p := GetPrincipal(ctx)
+
+		switch p := p.(type) {
+		case *UserPrincipal:
+			if p.GetIdentity(ctx) != "" {
+				(*md)[OAuth2IdentityMetadata] = []string{p.GetIdentity(ctx)}
+			}
+		case *ClientPrincipal:
+			if ident := p.GetIdentity(ctx); ident != nil {
+				switch {
+				case ident.OAuthClientID != "":
+					(*md)[OAuth2IdentityMetadata] = []string{ident.OAuthClientID + "@clients"}
+				case ident.TLSSubject != "":
+					(*md)[TLSIdentityMetadata] = []string{ident.TLSSubject}
+				case ident.APIKey != "":
+					(*md)[AuthorizationMetadata] = []string{"API-Key " + ident.APIKey}
+
+				}
+			}
+		case *SystemPrincipal:
+			(*md)[AccessMetadata] = []string{p.GetID(ctx)}
+		}
+
+		return ctx
+	}
+}
+
+// PrincipalServerInterceptor - grpc-интерсептор, который используется для получения данных принципала из grpc-метаданы и добавления в контекст ''. В случае, если
+// сервис не использует проверку прав 'Principal' к системе, в параметрах передается пустой объект '&PrincipalFactory{}'
+func PrincipalServerInterceptor(factory *PrincipalFactory) grpc.UnaryServerInterceptor {
+	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
+		if md, ok := metadata.FromIncomingContext(ctx); ok {
+			ctx = GRPCToContext(factory)(ctx, md)
+		}
+		return handler(ctx, req)
+	}
+}
+
+// PrincipalClientInterceptor - grpc-интерсептор, который используется для получения данных принципала. В случае, если
+// сервис не использует проверку прав 'Principal' к системе, в параметрах передается пустой объект '&PrincipalFactory{}'
+func PrincipalClientInterceptor() grpc.UnaryClientInterceptor {
+	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
+		md, ok := metadata.FromOutgoingContext(ctx)
+		if !ok {
+			md = metadata.MD{}
+		}
+		ctx = metadata.NewOutgoingContext(ContextToGRPC()(ctx, &md), md)
+		return invoker(ctx, method, req, reply, cc, opts...)
+	}
+}
diff --git a/pkg/auth/principal.go b/pkg/auth/principal.go
new file mode 100644
index 0000000000000000000000000000000000000000..78da09cb27f493f034720d40d91f33b7b6ce7313
--- /dev/null
+++ b/pkg/auth/principal.go
@@ -0,0 +1,86 @@
+package auth
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/data"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+)
+
+type Principal interface {
+	GetID(ctx context.Context) string
+	IsValid(ctx context.Context) bool
+	IsSystem(ctx context.Context) bool
+	HasAccess(ctx context.Context, spID, orgID string) error
+
+	IsManagementAllowed(ctx context.Context, spaceID string) error
+}
+
+type SpaceAccessor interface {
+	Principal
+	Space(spaceID string) SpaceAccessor
+
+	// HasSpaceAccess проверяет, есть ли у принципала доступ на чтение пространства
+	// (просмотр информации о пространстве, окружений, т.д. - доступ к записям коллекций
+	// определяется отдельным набором правил, см. SpaceAccessor.Rules())
+	HasSpaceAccess(ctx context.Context, spaceID string) bool
+	HasEnvironmentAccess(ctx context.Context, spaceID, env string) bool
+
+	// Member возвращает роль принципала в организации
+	Member(ctx context.Context) members.Role
+
+	Role(ctx context.Context, spaceID string) *roles.Role
+
+	// Rules возвращает набор правил, по которым принципал может получить
+	// доступ к записям коллекций пространства.
+	Rules(ctx context.Context, spaceID, envID string) permission.Ruleset
+}
+
+type OrganizationAccessor interface {
+	Principal
+	Organization(orgID string) OrganizationAccessor
+	Member(ctx context.Context) members.Role
+}
+
+func hasEnvironmentAccess(ctx context.Context, envsrv environments.Environments, role *roles.Role, envID string) bool {
+	if role == nil || role.SpaceID == "" || envID == "" {
+		return false
+	}
+
+	if role.AllowManagement {
+		return true
+	}
+
+	envs := role.Environments
+
+	// Если явно не указаны доступные окружения - доступ по умолчанию к окружению master
+	if len(envs) == 0 {
+		envs = []string{environments.DefaultEnvironment}
+	}
+
+	for _, ce := range envs {
+		if envID == ce || data.GlobMatch(envID, ce) {
+			return true
+		}
+	}
+
+	e, err := envsrv.Get(WithSystem(ctx), role.SpaceID, envID)
+	if err != nil || e == nil {
+		return false
+	}
+
+	aliases := append(e.Aliases, e.ID)
+
+	for _, ce := range envs {
+		for _, al := range aliases {
+			if al == ce || data.GlobMatch(al, ce) {
+				return true
+			}
+		}
+	}
+
+	return false
+}
diff --git a/pkg/auth/principal_test.go b/pkg/auth/principal_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..54e04ee4a8bf96151f9dfa9cf1edff0342ab03dd
--- /dev/null
+++ b/pkg/auth/principal_test.go
@@ -0,0 +1,178 @@
+package auth
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	mocksenvs "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"github.com/stretchr/testify/mock"
+)
+
+func Test_hasEnvironmentAccess(t *testing.T) {
+	type args struct {
+		ctx      context.Context
+		envscall func(envsservice *mocksenvs.Environments)
+		role     *roles.Role
+		envID    string
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "simple",
+			args: args{
+				ctx: context.Background(),
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"env1", "env2"},
+				},
+				envID: "env1",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: e*",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"e*"},
+				},
+				envID: "env",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: *n*",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"*n*"},
+				},
+				envID: "env",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: *1",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"*1"},
+				},
+				envID: "env",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test (alias): ma*",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"ma*"},
+				},
+				envID: "env1",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: *",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"*"},
+				},
+				envID: "env1",
+			},
+			want: true,
+		},
+		{
+			name: "glob env in role test: q*",
+			args: args{
+				ctx: context.Background(),
+				envscall: func(envsservice *mocksenvs.Environments) {
+					envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{
+						ID:      "env1",
+						SpaceID: "space",
+						Aliases: []string{"master"},
+					}, nil).Once()
+				},
+				role: &roles.Role{
+					ID:           "1",
+					SpaceID:      "space",
+					Description:  "Current",
+					Environments: []string{"q*"},
+				},
+				envID: "env1",
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			envsservice := &mocksenvs.Environments{}
+			if tt.args.envscall != nil {
+				tt.args.envscall(envsservice)
+			}
+
+			if got := hasEnvironmentAccess(tt.args.ctx, envsservice, tt.args.role, tt.args.envID); got != tt.want {
+				t.Errorf("hasEnvironmentAccess() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/auth/system.go b/pkg/auth/system.go
new file mode 100644
index 0000000000000000000000000000000000000000..b602fe114bb53d5f82ae5f6796806dc20ed75188
--- /dev/null
+++ b/pkg/auth/system.go
@@ -0,0 +1,39 @@
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+)
+
+type SystemPrincipal struct{}
+
+const (
+	SystemID = "system"
+)
+
+func (p SystemPrincipal) GetID(ctx context.Context) string                            { return SystemID }
+func (SystemPrincipal) IsValid(ctx context.Context) bool                              { return true }
+func (SystemPrincipal) IsSystem(ctx context.Context) bool                             { return true }
+func (SystemPrincipal) IsManagementAllowed(ctx context.Context, spaceID string) error { return nil }
+
+func (p SystemPrincipal) Organization(_ string) OrganizationAccessor { return p }
+
+func (p SystemPrincipal) Space(_ string) SpaceAccessor                  { return p }
+func (SystemPrincipal) HasSpaceAccess(_ context.Context, _ string) bool { return true }
+func (SystemPrincipal) HasAccess(ctx context.Context, spaceID, orgID string) error {
+	return nil
+}
+func (SystemPrincipal) HasEnvironmentAccess(_ context.Context, _, _ string) bool { return true }
+func (SystemPrincipal) Member(_ context.Context) members.Role                    { return members.NotMember }
+func (SystemPrincipal) Role(_ context.Context, _ string) *roles.Role             { return nil }
+func (SystemPrincipal) Rules(_ context.Context, _, _ string) permission.Ruleset {
+	return &permission.PrivilegedRuleset{}
+}
+
+func (SystemPrincipal) Format(f fmt.State, verb rune) {
+	f.Write([]byte("SystemPrincipal{}"))
+}
diff --git a/pkg/auth/user.go b/pkg/auth/user.go
new file mode 100644
index 0000000000000000000000000000000000000000..f34693bf93f9b63c5d5eca84398b096af86e52ef
--- /dev/null
+++ b/pkg/auth/user.go
@@ -0,0 +1,334 @@
+package auth
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+)
+
+type UserPrincipal struct {
+	id       string
+	identity string
+
+	user    *users.User
+	invalid bool
+	spaceID string
+	orgID   string
+
+	users         users.Users
+	members       members.Members
+	hasMemberRole bool
+	memberRole    members.Role
+
+	collaborators collaborators.Collaborators
+	spaces        spaces.Spaces
+	environments  environments.Environments
+	roles         roles.Roles
+}
+
+func (u UserPrincipal) Format(f fmt.State, verb rune) {
+	f.Write([]byte(fmt.Sprintf("UserPrincipal{ID: '%s', Identity: '%s'}", u.id, u.identity)))
+}
+
+func (u *UserPrincipal) Space(spaceID string) SpaceAccessor {
+	u.spaceID = spaceID
+	u.orgID = ""
+	return u
+}
+
+func (u *UserPrincipal) getSpace(ctx context.Context, spaceID string) *spaces.Space {
+	if spaceID == "" {
+		return nil
+	}
+	space, _ := u.spaces.Get(WithSystem(ctx), spaceID)
+	return space
+}
+
+func (u UserPrincipal) Organization(orgID string) OrganizationAccessor {
+	u.orgID = orgID
+	return &u
+}
+
+func (u *UserPrincipal) GetID(ctx context.Context) string {
+	user := u.User(ctx)
+	if user == nil {
+		return ""
+	}
+	return user.ID
+}
+
+func (u *UserPrincipal) GetIdentity(ctx context.Context) string {
+	return u.identity
+}
+
+func (u *UserPrincipal) IsValid(ctx context.Context) bool {
+	if u == nil {
+		return false
+	}
+
+	return u.User(ctx) != nil
+}
+
+func (u *UserPrincipal) IsSystem(ctx context.Context) bool {
+	user := u.User(ctx)
+	if user != nil {
+		return user.IsSystem()
+	}
+	return false
+}
+
+func (u *UserPrincipal) IsManagementAllowed(ctx context.Context, spaceID string) error {
+	if !u.IsValid(ctx) {
+		return ErrAccessDenied
+	}
+
+	if u.IsSystem(ctx) {
+		return nil
+	}
+
+	if u.Member(ctx).IsPrivileged() {
+		return nil
+	}
+
+	if role := u.Role(ctx, spaceID); role != nil && role.AllowManagement {
+		return nil
+	}
+
+	return ErrAccessDenied
+}
+
+func (u *UserPrincipal) User(ctx context.Context) *users.User {
+	if u.invalid {
+		return nil
+	}
+
+	if u.user != nil {
+		return u.user
+	}
+	if u.users == nil {
+		u.invalid = true
+		return nil
+	}
+
+	var user *users.User
+	var err error
+	switch {
+	case u.id != "":
+		user, err = u.users.Get(WithSystem(ctx), u.id)
+	case u.identity != "":
+		ctx = WithSystem(ctx)
+		user, err = u.users.GetByIdentity(WithSystem(ctx), u.identity)
+	}
+
+	if err != nil || user == nil {
+		u.invalid = true
+		return nil
+	}
+
+	u.user = user
+	return u.user
+}
+
+func (u *UserPrincipal) Member(ctx context.Context) members.Role {
+	if u.hasMemberRole {
+		return u.memberRole
+	}
+
+	if u.members == nil || (u.orgID == "" && u.spaceID == "") {
+		u.hasMemberRole = true
+		return members.NotMember
+	}
+
+	if u.orgID == "" && u.spaceID != "" {
+		sp := u.getSpace(ctx, u.spaceID)
+		if sp == nil {
+			u.hasMemberRole = true
+			return members.NotMember
+		}
+		u.orgID = sp.OrgID
+	}
+
+	role, err := u.members.Get(WithSystem(ctx), u.orgID, u.GetID(ctx))
+	if err != nil {
+		role = members.NotMember
+	}
+
+	u.memberRole = role
+	u.hasMemberRole = true
+	return u.memberRole
+}
+
+// HasSpaceAccess проверяет, есть ли у пользователя доступ к пространству
+// Пользователь имеет доступ к пространству если:
+// - Является участником пространства (даже если его роль не существует)
+// - Пространство позволяет доступ для не участников (есть роли AnonymousRole/AuthorizedRole/ViewRole)
+// Deprecated :use HasAccess
+func (u *UserPrincipal) HasSpaceAccess(ctx context.Context, spaceID string) bool {
+	res, _ := u.hasRole(ctx, spaceID)
+	return res
+}
+
+// HasAccess проверяет, есть ли у пользователя доступ к пространству
+// Пользователь имеет доступ к пространству если:
+// - Является участником пространства (даже если его роль не существует)
+// - Пространство позволяет доступ для не участников (есть роли AnonymousRole/AuthorizedRole/ViewRole)
+func (u *UserPrincipal) HasAccess(ctx context.Context, spaceID, orgID string) error {
+	if !u.IsValid(ctx) {
+		return ErrAccessDenied
+	}
+
+	if u.IsSystem(ctx) {
+		return nil
+	}
+
+	if spaceID != "" {
+		hasAllow, err := u.hasRole(ctx, spaceID)
+		if err != nil {
+			return err
+		}
+
+		if hasAllow {
+			return nil
+		}
+	}
+
+	if orgID != "" {
+		if u.Organization(orgID).Member(ctx).IsPrivileged() {
+			return nil
+		}
+	} else {
+		if u.Member(ctx).IsPrivileged() {
+			return nil
+		}
+	}
+
+	return ErrAccessDenied
+}
+
+func (u *UserPrincipal) hasRole(ctx context.Context, spaceID string) (bool, error) {
+
+	if u.spaceID == "" || spaceID == "" {
+		return false, nil
+	}
+
+	ctx = WithSystem(ctx)
+
+	if spaceID != u.spaceID {
+		_, cErr := u.collaborators.Get(ctx, spaceID, u.spaceID)
+		if cErr == nil {
+			return true, nil
+		}
+		_, rErr := u.roles.Get(ctx, spaceID, roles.ViewRole)
+		if rErr == nil {
+			return true, nil
+		}
+		if errors.Is(cErr, ErrNotFound) || errors.Is(rErr, ErrNotFound) {
+			if sp := u.getSpace(ctx, spaceID); sp == nil {
+				return false, ErrNotFound
+			}
+		}
+
+		return false, nil
+	}
+
+	_, cErr := u.collaborators.Get(ctx, spaceID, u.GetID(ctx))
+	if cErr == nil {
+		return true, nil
+	}
+
+	_, rErr := u.roles.Get(ctx, spaceID, roles.AuthorizedRole)
+	if rErr == nil {
+		return true, nil
+	}
+
+	if errors.Is(cErr, ErrNotFound) || errors.Is(rErr, ErrNotFound) {
+		if sp := u.getSpace(ctx, spaceID); sp == nil {
+			return false, ErrNotFound
+		}
+	}
+
+	return false, nil
+}
+
+func (u *UserPrincipal) getRoleID(ctx context.Context, spaceID string) string {
+
+	if u.spaceID == "" || spaceID == "" {
+		return ""
+	}
+
+	ctx = WithSystem(ctx)
+
+	if spaceID != u.spaceID {
+		rID, err := u.collaborators.Get(ctx, spaceID, u.spaceID)
+		if err != nil {
+			rID = roles.ViewRole
+		}
+		return rID
+	}
+
+	if roleID, err := u.collaborators.Get(ctx, spaceID, u.GetID(ctx)); err == nil {
+		return roleID
+	}
+
+	return roles.AuthorizedRole
+}
+
+func (u *UserPrincipal) Role(ctx context.Context, spaceID string) *roles.Role {
+
+	if roleID := u.getRoleID(ctx, spaceID); roleID != "" {
+		role, _ := u.roles.Get(WithSystem(ctx), spaceID, roleID)
+		return role
+	}
+
+	return nil
+}
+
+func (u *UserPrincipal) Rules(ctx context.Context, spaceID, envID string) permission.Ruleset {
+	if spaceID == "" || envID == "" {
+		return nil
+	}
+
+	if u.spaceID == spaceID && (u.IsSystem(ctx) || u.Member(ctx).IsPrivileged()) {
+		return permission.PrivilegedRuleset{}
+	}
+
+	role := u.Role(ctx, spaceID)
+	if role == nil {
+		return nil
+	}
+
+	if !hasEnvironmentAccess(ctx, u.environments, role, envID) {
+		return nil
+	}
+
+	return role.Rules
+}
+
+func IsValidUser(ctx context.Context, p Principal) bool {
+	if p == nil {
+		return false
+	}
+	if u, ok := p.(*UserPrincipal); ok {
+		return u.IsValid(ctx)
+	}
+	return false
+}
+
+func User(ctx context.Context, p Principal) *users.User {
+	if u, ok := p.(*UserPrincipal); ok {
+		return u.User(ctx)
+	}
+	return nil
+}
+
+func (u *UserPrincipal) HasEnvironmentAccess(ctx context.Context, spaceID, env string) bool {
+	return hasEnvironmentAccess(ctx, u.environments, u.Role(ctx, spaceID), env)
+}
diff --git a/pkg/environments/environment.go b/pkg/environments/environment.go
new file mode 100644
index 0000000000000000000000000000000000000000..cbd468dc361951e048a0840f4158a42c962e7af1
--- /dev/null
+++ b/pkg/environments/environment.go
@@ -0,0 +1,114 @@
+package environments
+
+import "time"
+
+const (
+	DefaultEnvironment = "master"
+)
+
+type State int
+
+const (
+	StateUnknown State = iota
+	StateNew
+	StatePreparing
+	StateReady
+	StateError
+
+	StateInfoEmpty = "EMPTY"
+)
+
+func (s State) String() string {
+	switch s {
+	case StateNew:
+		return "new"
+	case StatePreparing:
+		return "preparing"
+	case StateReady:
+		return "ready"
+	case StateError:
+		return "error"
+	default:
+		return "unknown"
+	}
+}
+
+type StateInfo struct {
+	State     State     `json:"state" bson:"state"`
+	StartedAt time.Time `json:"started_at,omitempty" bson:"started_at,omitempty"`
+	Info      string    `json:"info,omitempty" bson:"info,omitempty"`
+}
+
+type Config struct {
+	SourceID string
+
+	// Deprecated
+	Features []string
+}
+
+// Environment - представляет рабочее окружения для пространства
+// Каждое окружение может иметь собственный набор коллекций и данных и
+// использоваться независимо друг от друга
+type Environment struct {
+	ID          string `json:"id" bson:"_id"` // Идентификатор окружения, задается пользователем при создании. Уникален в рамках пространства `SpaceID`
+	SpaceID     string `json:"spaceID" bson:"-"`
+	Description string `json:"description" bson:"desc,omitempty"` // Описание для окружения
+	//State       State  `json:"state" bson:"state"`                // Состояние окружения (Preparing/Ready/Failed)
+	//StateInfo   string   `json:"state_info,omitempty" bson:"state_info,omitempty"`
+
+	// StateInfo отображает состояние коллекции:
+	// - State: идентификатор состояния окружения (unknown/new/preparing/ready/error)
+	// - Info: дополнительная информация о состоянии коллекции (например, если при
+	//   применении схемы к коллекции произошла ошибка)
+	// - StartedAt: время, в которое коллекция перешла в состояние `Preparing`
+	StateInfo *StateInfo `json:"state_info" bson:"state_info,omitempty"`
+
+	Aliases []string `json:"aliases" bson:"aliases,omitempty"` // Синонимы окружения (только чтение)
+	Config  *Config  `json:"config,omitempty" bson:"config,omitempty"`
+}
+
+func (e Environment) Clone() *Environment {
+	clone := &Environment{
+		ID:          e.ID,
+		SpaceID:     e.SpaceID,
+		Description: e.Description,
+		Aliases:     append([]string(nil), e.Aliases...),
+		Config:      nil,
+	}
+
+	if e.StateInfo != nil {
+		clone.StateInfo = &StateInfo{
+			State:     e.StateInfo.State,
+			Info:      e.StateInfo.Info,
+			StartedAt: e.StateInfo.StartedAt,
+		}
+	}
+
+	if e.Config != nil {
+		clone.Config = &Config{
+			SourceID: e.Config.SourceID,
+		}
+	}
+
+	return clone
+}
+
+func (e Environment) Fetch(i interface{}) interface{} {
+	p, _ := i.(string)
+	switch p {
+	case "ID":
+		return e.ID
+	case "SpaceID":
+		return e.SpaceID
+	case "Description":
+		return e.Description
+	case "StateInfo":
+		return e.StateInfo
+	case "Aliases":
+		return e.Aliases
+	case "Config":
+		return e.Config
+	default:
+		panic("unknown parameter")
+	}
+}
diff --git a/pkg/environments/mocks/Environments.go b/pkg/environments/mocks/Environments.go
new file mode 100644
index 0000000000000000000000000000000000000000..8c8d099f32cbcbd7cd1edf377b3d46e6756a1d72
--- /dev/null
+++ b/pkg/environments/mocks/Environments.go
@@ -0,0 +1,176 @@
+// Code generated by mockery v2.14.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	environments "git.perx.ru/perxis/perxis-go/pkg/environments"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Environments is an autogenerated mock type for the Environments type
+type Environments struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, env
+func (_m *Environments) Create(ctx context.Context, env *environments.Environment) (*environments.Environment, error) {
+	ret := _m.Called(ctx, env)
+
+	var r0 *environments.Environment
+	if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment) *environments.Environment); ok {
+		r0 = rf(ctx, env)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*environments.Environment)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *environments.Environment) error); ok {
+		r1 = rf(ctx, env)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, spaceId, envId
+func (_m *Environments) Delete(ctx context.Context, spaceId string, envId string) error {
+	ret := _m.Called(ctx, spaceId, envId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Get provides a mock function with given fields: ctx, spaceId, envId
+func (_m *Environments) Get(ctx context.Context, spaceId string, envId string) (*environments.Environment, error) {
+	ret := _m.Called(ctx, spaceId, envId)
+
+	var r0 *environments.Environment
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) *environments.Environment); ok {
+		r0 = rf(ctx, spaceId, envId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*environments.Environment)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
+		r1 = rf(ctx, spaceId, envId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// List provides a mock function with given fields: ctx, spaceId
+func (_m *Environments) List(ctx context.Context, spaceId string) ([]*environments.Environment, error) {
+	ret := _m.Called(ctx, spaceId)
+
+	var r0 []*environments.Environment
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*environments.Environment); ok {
+		r0 = rf(ctx, spaceId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*environments.Environment)
+		}
+	}
+
+	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
+}
+
+// Migrate provides a mock function with given fields: ctx, spaceId, envId, options
+func (_m *Environments) Migrate(ctx context.Context, spaceId string, envId string, options ...*environments.MigrateOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, spaceId, envId)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, ...*environments.MigrateOptions) error); ok {
+		r0 = rf(ctx, spaceId, envId, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// RemoveAlias provides a mock function with given fields: ctx, spaceId, envId, alias
+func (_m *Environments) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) error {
+	ret := _m.Called(ctx, spaceId, envId, alias)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId, alias)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// SetAlias provides a mock function with given fields: ctx, spaceId, envId, alias
+func (_m *Environments) SetAlias(ctx context.Context, spaceId string, envId string, alias string) error {
+	ret := _m.Called(ctx, spaceId, envId, alias)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId, alias)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, env
+func (_m *Environments) Update(ctx context.Context, env *environments.Environment) error {
+	ret := _m.Called(ctx, env)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment) error); ok {
+		r0 = rf(ctx, env)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewEnvironments interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewEnvironments creates a new instance of Environments. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewEnvironments(t mockConstructorTestingTNewEnvironments) *Environments {
+	mock := &Environments{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/environments/options.go b/pkg/environments/options.go
new file mode 100644
index 0000000000000000000000000000000000000000..02f1a1fea3229ccfe702401c2dfa06e5094d9671
--- /dev/null
+++ b/pkg/environments/options.go
@@ -0,0 +1,37 @@
+package environments
+
+type MigrateOptions struct {
+
+	// Ожидать завершения миграции в синхронном режиме
+	Wait bool
+}
+
+func MergeMigrateOptions(opts ...*MigrateOptions) *MigrateOptions {
+	o := &MigrateOptions{}
+	for _, opt := range opts {
+		if opt.Wait {
+			o.Wait = true
+		}
+	}
+	return o
+}
+
+type UpdateOptions struct {
+
+	// Состояние будет обновлено только в том случае, если выполняется указанное условие
+	// Cond указывается с использованием синтаксиса `expr`
+	Cond string
+}
+
+func MergeUpdateOptions(opts ...*UpdateOptions) *UpdateOptions {
+	o := &UpdateOptions{}
+	for _, opt := range opts {
+		if opt.Cond != "" {
+			if o.Cond != "" {
+				o.Cond += " && "
+			}
+			o.Cond += opt.Cond
+		}
+	}
+	return o
+}
diff --git a/pkg/environments/service.go b/pkg/environments/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..cd0a37f449819a4b92239f8d8b38a3a0036ec8d8
--- /dev/null
+++ b/pkg/environments/service.go
@@ -0,0 +1,20 @@
+package environments
+
+import (
+	"context"
+)
+
+// Environments
+// @microgen grpc
+// @protobuf git.perx.ru/perxis/perxis-go/proto/environments
+// @grpc-addr content.environments.Environments
+type Environments interface {
+	Create(ctx context.Context, env *Environment) (created *Environment, err error)
+	Get(ctx context.Context, spaceId, envId string) (env *Environment, err error)
+	List(ctx context.Context, spaceId string) (envs []*Environment, err error)
+	Update(ctx context.Context, env *Environment) (err error)
+	Delete(ctx context.Context, spaceId, envId string) (err error)
+	SetAlias(ctx context.Context, spaceId, envId, alias string) (err error)
+	RemoveAlias(ctx context.Context, spaceId, envId, alias string) (err error)
+	Migrate(ctx context.Context, spaceId, envId string, options ...*MigrateOptions) (err error)
+}
diff --git a/pkg/environments/transport/client.microgen.go b/pkg/environments/transport/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..96094fda943e407d0264a8abc738fd5c3afe440e
--- /dev/null
+++ b/pkg/environments/transport/client.microgen.go
@@ -0,0 +1,126 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	"errors"
+
+	environments "git.perx.ru/perxis/perxis-go/pkg/environments"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+func (set EndpointsSet) Create(arg0 context.Context, arg1 *environments.Environment) (res0 *environments.Environment, res1 error) {
+	request := CreateRequest{Env: 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 *environments.Environment, res1 error) {
+	request := GetRequest{
+		EnvId:   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).Env, res1
+}
+
+func (set EndpointsSet) List(arg0 context.Context, arg1 string) (res0 []*environments.Environment, 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).Envs, res1
+}
+
+func (set EndpointsSet) Update(arg0 context.Context, arg1 *environments.Environment) (res0 error) {
+	request := UpdateRequest{Env: 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{
+		EnvId:   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) SetAlias(arg0 context.Context, arg1 string, arg2 string, arg3 string) (res0 error) {
+	request := SetAliasRequest{
+		Alias:   arg3,
+		EnvId:   arg2,
+		SpaceId: arg1,
+	}
+	_, res0 = set.SetAliasEndpoint(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) RemoveAlias(arg0 context.Context, arg1 string, arg2 string, arg3 string) (res0 error) {
+	request := RemoveAliasRequest{
+		Alias:   arg3,
+		EnvId:   arg2,
+		SpaceId: arg1,
+	}
+	_, res0 = set.RemoveAliasEndpoint(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) Migrate(arg0 context.Context, arg1 string, arg2 string, arg3 ...*environments.MigrateOptions) (res0 error) {
+	request := MigrateRequest{
+		EnvId:   arg2,
+		Options: arg3,
+		SpaceId: arg1,
+	}
+	_, res0 = set.MigrateEndpoint(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/environments/transport/endpoints.microgen.go b/pkg/environments/transport/endpoints.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..5639637d2b479721e21d5c3fe4ea097d2f2a6cde
--- /dev/null
+++ b/pkg/environments/transport/endpoints.microgen.go
@@ -0,0 +1,17 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import endpoint "github.com/go-kit/kit/endpoint"
+
+// EndpointsSet implements Environments API and used for transport purposes.
+type EndpointsSet struct {
+	CreateEndpoint      endpoint.Endpoint
+	GetEndpoint         endpoint.Endpoint
+	ListEndpoint        endpoint.Endpoint
+	UpdateEndpoint      endpoint.Endpoint
+	DeleteEndpoint      endpoint.Endpoint
+	SetAliasEndpoint    endpoint.Endpoint
+	RemoveAliasEndpoint endpoint.Endpoint
+	MigrateEndpoint     endpoint.Endpoint
+}
diff --git a/pkg/environments/transport/exchanges.microgen.go b/pkg/environments/transport/exchanges.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..a1a0ab068d575767c543d8db652c5d8c9a7e02f4
--- /dev/null
+++ b/pkg/environments/transport/exchanges.microgen.go
@@ -0,0 +1,66 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import environments "git.perx.ru/perxis/perxis-go/pkg/environments"
+
+type (
+	CreateRequest struct {
+		Env *environments.Environment `json:"env"`
+	}
+	CreateResponse struct {
+		Created *environments.Environment `json:"created"`
+	}
+
+	GetRequest struct {
+		SpaceId string `json:"space_id"`
+		EnvId   string `json:"env_id"`
+	}
+	GetResponse struct {
+		Env *environments.Environment `json:"env"`
+	}
+
+	ListRequest struct {
+		SpaceId string `json:"space_id"`
+	}
+	ListResponse struct {
+		Envs []*environments.Environment `json:"envs"`
+	}
+
+	UpdateRequest struct {
+		Env *environments.Environment `json:"env"`
+	}
+	// Formal exchange type, please do not delete.
+	UpdateResponse struct{}
+
+	DeleteRequest struct {
+		SpaceId string `json:"space_id"`
+		EnvId   string `json:"env_id"`
+	}
+	// Formal exchange type, please do not delete.
+	DeleteResponse struct{}
+
+	SetAliasRequest struct {
+		SpaceId string `json:"space_id"`
+		EnvId   string `json:"env_id"`
+		Alias   string `json:"alias"`
+	}
+	// Formal exchange type, please do not delete.
+	SetAliasResponse struct{}
+
+	RemoveAliasRequest struct {
+		SpaceId string `json:"space_id"`
+		EnvId   string `json:"env_id"`
+		Alias   string `json:"alias"`
+	}
+	// Formal exchange type, please do not delete.
+	RemoveAliasResponse struct{}
+
+	MigrateRequest struct {
+		SpaceId string                         `json:"space_id"`
+		EnvId   string                         `json:"env_id"`
+		Options []*environments.MigrateOptions `json:"options"` // This field was defined with ellipsis (...).
+	}
+	// Formal exchange type, please do not delete.
+	MigrateResponse struct{}
+)
diff --git a/pkg/environments/transport/grpc/client.microgen.go b/pkg/environments/transport/grpc/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..34e0fa4db1cf8149475dde562b3179091089f70f
--- /dev/null
+++ b/pkg/environments/transport/grpc/client.microgen.go
@@ -0,0 +1,75 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/environments/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/environments"
+	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.environments.Environments"
+	}
+	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(),
+		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(),
+		MigrateEndpoint: grpckit.NewClient(
+			conn, addr, "Migrate",
+			_Encode_Migrate_Request,
+			_Decode_Migrate_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+		RemoveAliasEndpoint: grpckit.NewClient(
+			conn, addr, "RemoveAlias",
+			_Encode_RemoveAlias_Request,
+			_Decode_RemoveAlias_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+		SetAliasEndpoint: grpckit.NewClient(
+			conn, addr, "SetAlias",
+			_Encode_SetAlias_Request,
+			_Decode_SetAlias_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+		UpdateEndpoint: grpckit.NewClient(
+			conn, addr, "Update",
+			_Encode_Update_Request,
+			_Decode_Update_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+	}
+}
diff --git a/pkg/environments/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/environments/transport/grpc/protobuf_endpoint_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..6411216cd4f54cedd9bf2a3c61ff77b98d70cc35
--- /dev/null
+++ b/pkg/environments/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -0,0 +1,307 @@
+// 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/environments/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/environments"
+	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)
+	reqEnv, err := PtrEnvironmentToProto(req.Env)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateRequest{Env: reqEnv}, 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{
+		EnvId:   req.EnvId,
+		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_Update_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil UpdateRequest")
+	}
+	req := request.(*transport.UpdateRequest)
+	reqEnv, err := PtrEnvironmentToProto(req.Env)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.UpdateRequest{Env: reqEnv}, 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{
+		EnvId:   req.EnvId,
+		SpaceId: req.SpaceId,
+	}, nil
+}
+
+func _Encode_SetAlias_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil SetAliasRequest")
+	}
+	req := request.(*transport.SetAliasRequest)
+	return &pb.SetAliasRequest{
+		Alias:   req.Alias,
+		EnvId:   req.EnvId,
+		SpaceId: req.SpaceId,
+	}, nil
+}
+
+func _Encode_RemoveAlias_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil RemoveAliasRequest")
+	}
+	req := request.(*transport.RemoveAliasRequest)
+	return &pb.RemoveAliasRequest{
+		Alias:   req.Alias,
+		EnvId:   req.EnvId,
+		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)
+	respCreated, err := PtrEnvironmentToProto(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateResponse{Created: respCreated}, 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)
+	respEnv, err := PtrEnvironmentToProto(resp.Env)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.GetResponse{Env: respEnv}, 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)
+	respEnvs, err := ListPtrEnvironmentToProto(resp.Envs)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.ListResponse{Envs: respEnvs}, 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_SetAlias_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Encode_RemoveAlias_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)
+	reqEnv, err := ProtoToPtrEnvironment(req.Env)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateRequest{Env: reqEnv}, 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{
+		EnvId:   string(req.EnvId),
+		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_Update_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil UpdateRequest")
+	}
+	req := request.(*pb.UpdateRequest)
+	reqEnv, err := ProtoToPtrEnvironment(req.Env)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.UpdateRequest{Env: reqEnv}, 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{
+		EnvId:   string(req.EnvId),
+		SpaceId: string(req.SpaceId),
+	}, nil
+}
+
+func _Decode_SetAlias_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil SetAliasRequest")
+	}
+	req := request.(*pb.SetAliasRequest)
+	return &transport.SetAliasRequest{
+		Alias:   string(req.Alias),
+		EnvId:   string(req.EnvId),
+		SpaceId: string(req.SpaceId),
+	}, nil
+}
+
+func _Decode_RemoveAlias_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil RemoveAliasRequest")
+	}
+	req := request.(*pb.RemoveAliasRequest)
+	return &transport.RemoveAliasRequest{
+		Alias:   string(req.Alias),
+		EnvId:   string(req.EnvId),
+		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)
+	respCreated, err := ProtoToPtrEnvironment(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateResponse{Created: respCreated}, 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)
+	respEnv, err := ProtoToPtrEnvironment(resp.Env)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.GetResponse{Env: respEnv}, 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)
+	respEnvs, err := ProtoToListPtrEnvironment(resp.Envs)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.ListResponse{Envs: respEnvs}, 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_SetAlias_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_RemoveAlias_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Encode_Migrate_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil MigrateRequest")
+	}
+	req := request.(*transport.MigrateRequest)
+	opts, _ := ElPtrMigrateOptionsToProto(req.Options)
+	return &pb.MigrateRequest{
+		EnvId:   req.EnvId,
+		SpaceId: req.SpaceId,
+		Options: opts,
+	}, nil
+}
+
+func _Encode_Migrate_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_Migrate_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil MigrateRequest")
+	}
+	req := request.(*pb.MigrateRequest)
+	opts, _ := ProtoToElPtrMigrateOptions(req.Options)
+	return &transport.MigrateRequest{
+		EnvId:   string(req.EnvId),
+		SpaceId: string(req.SpaceId),
+		Options: opts,
+	}, nil
+}
+
+func _Decode_Migrate_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
diff --git a/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go b/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..5402c513bc7ecbf3a7dc3b4a291282eed03f554e
--- /dev/null
+++ b/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go
@@ -0,0 +1,99 @@
+// 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 (
+	pb "git.perx.ru/perxis/perxis-go/proto/environments"
+	service "git.perx.ru/perxis/perxis-go/pkg/environments"
+	"github.com/golang/protobuf/ptypes"
+)
+
+func PtrEnvironmentToProto(env *service.Environment) (*pb.Environment, error) {
+	if env == nil {
+		return nil, nil
+	}
+	protoEnvironment := &pb.Environment{
+		Id:          env.ID,
+		SpaceId:     env.SpaceID,
+		Description: env.Description,
+		Aliases:     env.Aliases,
+	}
+	if env.StateInfo != nil {
+		protoEnvironment.StateInfo = &pb.StateInfo{
+			State: pb.StateInfo_State(env.StateInfo.State),
+			Info:  env.StateInfo.Info,
+		}
+		protoEnvironment.StateInfo.StartedAt, _ = ptypes.TimestampProto(env.StateInfo.StartedAt)
+	}
+	if env.Config != nil {
+		protoEnvironment.Config = &pb.Config{
+			SourceId: env.Config.SourceID,
+			Features: env.Config.Features,
+		}
+	}
+	return protoEnvironment, nil
+}
+
+func ProtoToPtrEnvironment(protoEnv *pb.Environment) (*service.Environment, error) {
+	if protoEnv == nil {
+		return nil, nil
+	}
+	env := &service.Environment{
+		ID:          protoEnv.Id,
+		SpaceID:     protoEnv.SpaceId,
+		Description: protoEnv.Description,
+		Aliases:     protoEnv.Aliases,
+	}
+	if protoEnv.StateInfo != nil {
+		env.StateInfo = &service.StateInfo{
+			State: service.State(protoEnv.StateInfo.State),
+			Info:  protoEnv.StateInfo.Info,
+		}
+		env.StateInfo.StartedAt, _ = ptypes.Timestamp(protoEnv.StateInfo.StartedAt)
+	}
+	if protoEnv.Config != nil {
+		env.Config = &service.Config{
+			SourceID: protoEnv.Config.SourceId,
+			Features: protoEnv.Config.Features,
+		}
+	}
+	return env, nil
+}
+
+func ListPtrEnvironmentToProto(envs []*service.Environment) ([]*pb.Environment, error) {
+	protoEnvironments := make([]*pb.Environment, 0, len(envs))
+	for _, environment := range envs {
+		protoEnvironment, err := PtrEnvironmentToProto(environment)
+		if err != nil {
+			return nil, err
+		}
+		protoEnvironments = append(protoEnvironments, protoEnvironment)
+	}
+	return protoEnvironments, nil
+}
+
+func ProtoToListPtrEnvironment(protoEnvs []*pb.Environment) ([]*service.Environment, error) {
+	environments := make([]*service.Environment, 0, len(protoEnvs))
+	for _, protoEnvironment := range protoEnvs {
+		environment, err := ProtoToPtrEnvironment(protoEnvironment)
+		if err != nil {
+			return nil, err
+		}
+		environments = append(environments, environment)
+	}
+	return environments, nil
+}
+
+func ElPtrMigrateOptionsToProto(options []*service.MigrateOptions) (*pb.MigrateOptions, error) {
+	opts := service.MergeMigrateOptions(options...)
+	return &pb.MigrateOptions{Wait: opts.Wait}, nil
+}
+
+func ProtoToElPtrMigrateOptions(protoOptions *pb.MigrateOptions) ([]*service.MigrateOptions, error) {
+	if protoOptions == nil {
+		return nil, nil
+	}
+	return []*service.MigrateOptions{{Wait: protoOptions.Wait}}, nil
+}
diff --git a/pkg/environments/transport/grpc/server.microgen.go b/pkg/environments/transport/grpc/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..1389d32692eaf27f5068073d330696d1ad221bd3
--- /dev/null
+++ b/pkg/environments/transport/grpc/server.microgen.go
@@ -0,0 +1,142 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+// DO NOT EDIT.
+package transportgrpc
+
+import (
+	transport "git.perx.ru/perxis/perxis-go/pkg/environments/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/environments"
+	grpc "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	context "golang.org/x/net/context"
+)
+
+type environmentsServer struct {
+	create      grpc.Handler
+	get         grpc.Handler
+	list        grpc.Handler
+	update      grpc.Handler
+	delete      grpc.Handler
+	setAlias    grpc.Handler
+	removeAlias grpc.Handler
+	migrate     grpc.Handler
+
+	pb.UnimplementedEnvironmentsServer
+}
+
+func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.EnvironmentsServer {
+	return &environmentsServer{
+		create: grpc.NewServer(
+			endpoints.CreateEndpoint,
+			_Decode_Create_Request,
+			_Encode_Create_Response,
+			opts...,
+		),
+		delete: grpc.NewServer(
+			endpoints.DeleteEndpoint,
+			_Decode_Delete_Request,
+			_Encode_Delete_Response,
+			opts...,
+		),
+		get: grpc.NewServer(
+			endpoints.GetEndpoint,
+			_Decode_Get_Request,
+			_Encode_Get_Response,
+			opts...,
+		),
+		list: grpc.NewServer(
+			endpoints.ListEndpoint,
+			_Decode_List_Request,
+			_Encode_List_Response,
+			opts...,
+		),
+		migrate: grpc.NewServer(
+			endpoints.MigrateEndpoint,
+			_Decode_Migrate_Request,
+			_Encode_Migrate_Response,
+			opts...,
+		),
+		removeAlias: grpc.NewServer(
+			endpoints.RemoveAliasEndpoint,
+			_Decode_RemoveAlias_Request,
+			_Encode_RemoveAlias_Response,
+			opts...,
+		),
+		setAlias: grpc.NewServer(
+			endpoints.SetAliasEndpoint,
+			_Decode_SetAlias_Request,
+			_Encode_SetAlias_Response,
+			opts...,
+		),
+		update: grpc.NewServer(
+			endpoints.UpdateEndpoint,
+			_Decode_Update_Request,
+			_Encode_Update_Response,
+			opts...,
+		),
+	}
+}
+
+func (S *environmentsServer) 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 *environmentsServer) 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 *environmentsServer) 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 *environmentsServer) 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 *environmentsServer) 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 *environmentsServer) SetAlias(ctx context.Context, req *pb.SetAliasRequest) (*empty.Empty, error) {
+	_, resp, err := S.setAlias.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
+
+func (S *environmentsServer) RemoveAlias(ctx context.Context, req *pb.RemoveAliasRequest) (*empty.Empty, error) {
+	_, resp, err := S.removeAlias.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
+
+func (S *environmentsServer) Migrate(ctx context.Context, req *pb.MigrateRequest) (*empty.Empty, error) {
+	_, resp, err := S.migrate.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
diff --git a/pkg/environments/transport/server.microgen.go b/pkg/environments/transport/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..3a0b20a55cdf3aff82beb803efb59c5833e5afd7
--- /dev/null
+++ b/pkg/environments/transport/server.microgen.go
@@ -0,0 +1,88 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+
+"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	endpoint "github.com/go-kit/kit/endpoint"
+)
+
+func Endpoints(svc environments.Environments) EndpointsSet {
+	return EndpointsSet{
+		CreateEndpoint:      CreateEndpoint(svc),
+		DeleteEndpoint:      DeleteEndpoint(svc),
+		GetEndpoint:         GetEndpoint(svc),
+		ListEndpoint:        ListEndpoint(svc),
+		MigrateEndpoint:     MigrateEndpoint(svc),
+		RemoveAliasEndpoint: RemoveAliasEndpoint(svc),
+		SetAliasEndpoint:    SetAliasEndpoint(svc),
+		UpdateEndpoint:      UpdateEndpoint(svc),
+	}
+}
+
+func CreateEndpoint(svc environments.Environments) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*CreateRequest)
+		res0, res1 := svc.Create(arg0, req.Env)
+		return &CreateResponse{Created: res0}, res1
+	}
+}
+
+func GetEndpoint(svc environments.Environments) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetRequest)
+		res0, res1 := svc.Get(arg0, req.SpaceId, req.EnvId)
+		return &GetResponse{Env: res0}, res1
+	}
+}
+
+func ListEndpoint(svc environments.Environments) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*ListRequest)
+		res0, res1 := svc.List(arg0, req.SpaceId)
+		return &ListResponse{Envs: res0}, res1
+	}
+}
+
+func UpdateEndpoint(svc environments.Environments) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*UpdateRequest)
+		res0 := svc.Update(arg0, req.Env)
+		return &UpdateResponse{}, res0
+	}
+}
+
+func DeleteEndpoint(svc environments.Environments) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*DeleteRequest)
+		res0 := svc.Delete(arg0, req.SpaceId, req.EnvId)
+		return &DeleteResponse{}, res0
+	}
+}
+
+func SetAliasEndpoint(svc environments.Environments) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*SetAliasRequest)
+		res0 := svc.SetAlias(arg0, req.SpaceId, req.EnvId, req.Alias)
+		return &SetAliasResponse{}, res0
+	}
+}
+
+func RemoveAliasEndpoint(svc environments.Environments) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*RemoveAliasRequest)
+		res0 := svc.RemoveAlias(arg0, req.SpaceId, req.EnvId, req.Alias)
+		return &RemoveAliasResponse{}, res0
+	}
+}
+
+func MigrateEndpoint(svc environments.Environments) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*MigrateRequest)
+		res0 := svc.Migrate(arg0, req.SpaceId, req.EnvId, req.Options...)
+		return &MigrateResponse{}, res0
+	}
+}
diff --git a/pkg/invitations/invitation.go b/pkg/invitations/invitation.go
new file mode 100644
index 0000000000000000000000000000000000000000..5dc5913774fc5774076823309abdf52b195c87c2
--- /dev/null
+++ b/pkg/invitations/invitation.go
@@ -0,0 +1,16 @@
+package invitations
+
+import "time"
+
+const InvitationTTL = 7 * 24 * time.Hour
+
+type Invitation struct {
+	ID         string     `bson:"_id"`
+	Email      string     `bson:"email"`
+	OrgID      string     `bson:"orgId"`
+	SpaceID    string     `bson:"spaceId"`
+	OwnerID    string     `bson:"ownerId"` // Invitation owner
+	Role       string     `bson:"role"`
+	CreatedAt  *time.Time `bson:"createdAt"`
+	ValidUntil *time.Time `bson:"validUntil"`
+}
diff --git a/pkg/invitations/mocks/Invitations.go b/pkg/invitations/mocks/Invitations.go
new file mode 100644
index 0000000000000000000000000000000000000000..610f9fff80d4867b35b80ff464bcf2a8a9d0e764
--- /dev/null
+++ b/pkg/invitations/mocks/Invitations.go
@@ -0,0 +1,120 @@
+// Code generated by mockery v2.7.4. DO NOT EDIT.
+
+package mocks
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/invitations"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"github.com/stretchr/testify/mock"
+)
+
+// Invitations is an autogenerated mock type for the Invitations type
+type Invitations struct {
+	mock.Mock
+}
+
+// Accept provides a mock function with given fields: ctx, invitationId, userId
+func (_m *Invitations) Accept(ctx context.Context, invitationId string, userId string) error {
+	ret := _m.Called(ctx, invitationId, userId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, invitationId, userId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Create provides a mock function with given fields: ctx, invitation
+func (_m *Invitations) Create(ctx context.Context, invitation *invitations.Invitation) (*invitations.Invitation, error) {
+	ret := _m.Called(ctx, invitation)
+
+	var r0 *invitations.Invitation
+	if rf, ok := ret.Get(0).(func(context.Context, *invitations.Invitation) *invitations.Invitation); ok {
+		r0 = rf(ctx, invitation)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*invitations.Invitation)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *invitations.Invitation) error); ok {
+		r1 = rf(ctx, invitation)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, invitationId
+func (_m *Invitations) Delete(ctx context.Context, invitationId string) error {
+	ret := _m.Called(ctx, invitationId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, invitationId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Find provides a mock function with given fields: ctx, filter, opts
+func (_m *Invitations) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) ([]*invitations.Invitation, int, error) {
+	ret := _m.Called(ctx, filter, opts)
+
+	var r0 []*invitations.Invitation
+	if rf, ok := ret.Get(0).(func(context.Context, *invitations.Filter, *options.FindOptions) []*invitations.Invitation); ok {
+		r0 = rf(ctx, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*invitations.Invitation)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *invitations.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, *invitations.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, invitationId
+func (_m *Invitations) Get(ctx context.Context, invitationId string) (*invitations.Invitation, error) {
+	ret := _m.Called(ctx, invitationId)
+
+	var r0 *invitations.Invitation
+	if rf, ok := ret.Get(0).(func(context.Context, string) *invitations.Invitation); ok {
+		r0 = rf(ctx, invitationId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*invitations.Invitation)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, invitationId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
diff --git a/pkg/invitations/service.go b/pkg/invitations/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..26426d67462cdcb51fc7fe8f1bd60dc1fde69260
--- /dev/null
+++ b/pkg/invitations/service.go
@@ -0,0 +1,27 @@
+package invitations
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Filter struct {
+	ID      []string
+	Email   []string
+	OrgID   []string
+	SpaceID []string
+	OwnerID []string
+	Role    []string
+}
+
+// @microgen grpc
+// @protobuf git.perx.ru/perxis/perxis-go/proto/invitations
+// @grpc-addr content.invitations.Invitations
+type Invitations interface {
+	Create(ctx context.Context, invitation *Invitation) (created *Invitation, err error)
+	Get(ctx context.Context, invitationId string) (invitation *Invitation, err error)
+	Accept(ctx context.Context, invitationId, userId string) (err error)
+	Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (invitations []*Invitation, total int, err error)
+	Delete(ctx context.Context, invitationId string) (err error)
+}
diff --git a/pkg/invitations/transport/client.microgen.go b/pkg/invitations/transport/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..6f896b0e67fa88a1eaa539c305a47fada11c9c8b
--- /dev/null
+++ b/pkg/invitations/transport/client.microgen.go
@@ -0,0 +1,79 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	"errors"
+
+	invitations "git.perx.ru/perxis/perxis-go/pkg/invitations"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+func (set EndpointsSet) Create(arg0 context.Context, arg1 *invitations.Invitation) (res0 *invitations.Invitation, res1 error) {
+	request := CreateRequest{Invitation: 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) (res0 *invitations.Invitation, res1 error) {
+	request := GetRequest{InvitationId: 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).Invitation, res1
+}
+
+func (set EndpointsSet) Accept(arg0 context.Context, arg1 string, arg2 string) (res0 error) {
+	request := AcceptRequest{
+		InvitationId: arg1,
+		UserId:       arg2,
+	}
+	_, res0 = set.AcceptEndpoint(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) Find(arg0 context.Context, arg1 *invitations.Filter, arg2 *options.FindOptions) (res0 []*invitations.Invitation, res1 int, res2 error) {
+	request := FindRequest{
+		Filter: arg1,
+		Opts:   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).Invitations, response.(*FindResponse).Total, res2
+}
+
+func (set EndpointsSet) Delete(arg0 context.Context, arg1 string) (res0 error) {
+	request := DeleteRequest{InvitationId: 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/invitations/transport/endpoints.microgen.go b/pkg/invitations/transport/endpoints.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..70d78bbcf04e68ad97c36d90b4f66f3287f108d2
--- /dev/null
+++ b/pkg/invitations/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 Invitations API and used for transport purposes.
+type EndpointsSet struct {
+	CreateEndpoint endpoint.Endpoint
+	GetEndpoint    endpoint.Endpoint
+	AcceptEndpoint endpoint.Endpoint
+	FindEndpoint   endpoint.Endpoint
+	DeleteEndpoint endpoint.Endpoint
+}
diff --git a/pkg/invitations/transport/exchanges.microgen.go b/pkg/invitations/transport/exchanges.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..bb36e466cbcb25584c462931a3eab55ffd90732c
--- /dev/null
+++ b/pkg/invitations/transport/exchanges.microgen.go
@@ -0,0 +1,46 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/invitations"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type (
+	CreateRequest struct {
+		Invitation *invitations.Invitation `json:"invitation"`
+	}
+	CreateResponse struct {
+		Created *invitations.Invitation `json:"created"`
+	}
+
+	GetRequest struct {
+		InvitationId string `json:"invitation_id"`
+	}
+	GetResponse struct {
+		Invitation *invitations.Invitation `json:"invitation"`
+	}
+
+	AcceptRequest struct {
+		InvitationId string `json:"invitation_id"`
+		UserId       string `json:"user_id"`
+	}
+	// Formal exchange type, please do not delete.
+	AcceptResponse struct{}
+
+	FindRequest struct {
+		Filter *invitations.Filter  `json:"filter"`
+		Opts   *options.FindOptions `json:"opts"`
+	}
+	FindResponse struct {
+		Invitations []*invitations.Invitation `json:"invitations"`
+		Total       int                       `json:"total"`
+	}
+
+	DeleteRequest struct {
+		InvitationId string `json:"invitation_id"`
+	}
+	// Formal exchange type, please do not delete.
+	DeleteResponse struct{}
+)
diff --git a/pkg/invitations/transport/grpc/client.microgen.go b/pkg/invitations/transport/grpc/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..e319e19f9d32b9d3efa1e0d6fe485372e177efa2
--- /dev/null
+++ b/pkg/invitations/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/invitations/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/invitations"
+	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.invitations.Invitations"
+	}
+	return transport.EndpointsSet{
+		AcceptEndpoint: grpckit.NewClient(
+			conn, addr, "Accept",
+			_Encode_Accept_Request,
+			_Decode_Accept_Response,
+			empty.Empty{},
+			opts...,
+		).Endpoint(),
+		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(),
+		GetEndpoint: grpckit.NewClient(
+			conn, addr, "Get",
+			_Encode_Get_Request,
+			_Decode_Get_Response,
+			pb.GetResponse{},
+			opts...,
+		).Endpoint(),
+	}
+}
diff --git a/pkg/invitations/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/invitations/transport/grpc/protobuf_endpoint_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..85403cab8c13fb71612e275a2851c02a904d8abc
--- /dev/null
+++ b/pkg/invitations/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -0,0 +1,223 @@
+// 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/invitations/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/invitations"
+	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)
+	pbInvitation, err := PtrInvitationToProto(req.Invitation)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateRequest{Invitation: pbInvitation}, 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{InvitationId: req.InvitationId}, nil
+}
+
+func _Encode_Accept_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil AcceptRequest")
+	}
+	req := request.(*transport.AcceptRequest)
+	return &pb.AcceptRequest{
+		InvitationId: req.InvitationId,
+		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
+	}
+	reqOpts, err := PtrServicesFindOptionsToProto(req.Opts)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.FindRequest{
+		Filter: reqFilter,
+		Opts:   reqOpts,
+	}, 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{InvitationId: req.InvitationId}, 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)
+	respInvitation, err := PtrInvitationToProto(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateResponse{Invitation: respInvitation}, 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)
+	respInvitation, err := PtrInvitationToProto(resp.Invitation)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.GetResponse{Invitation: respInvitation}, nil
+}
+
+func _Encode_Accept_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, 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)
+	respInvitations, err := ListPtrInvitationToProto(resp.Invitations)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.FindResponse{
+		Invitations: respInvitations,
+		Total:       int64(resp.Total),
+	}, 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)
+	invitation, err := ProtoToPtrInvitation(req.Invitation)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateRequest{Invitation: invitation}, 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{InvitationId: string(req.InvitationId)}, nil
+}
+
+func _Decode_Accept_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil AcceptRequest")
+	}
+	req := request.(*pb.AcceptRequest)
+	return &transport.AcceptRequest{
+		InvitationId: string(req.InvitationId),
+		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
+	}
+	reqOpts, err := ProtoToPtrServicesFindOptions(req.Opts)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.FindRequest{
+		Filter: reqFilter,
+		Opts:   reqOpts,
+	}, 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{InvitationId: string(req.InvitationId)}, 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)
+	respInvitation, err := ProtoToPtrInvitation(resp.Invitation)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateResponse{Created: respInvitation}, 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)
+	respInvitation, err := ProtoToPtrInvitation(resp.Invitation)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.GetResponse{Invitation: respInvitation}, nil
+}
+
+func _Decode_Accept_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, 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)
+	respInvitations, err := ProtoToListPtrInvitation(resp.Invitations)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.FindResponse{
+		Invitations: respInvitations,
+		Total:       int(resp.Total),
+	}, nil
+}
+
+func _Decode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
diff --git a/pkg/invitations/transport/grpc/protobuf_type_converters.microgen.go b/pkg/invitations/transport/grpc/protobuf_type_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..9899e39885a8ca07de15f74f2eeda24d65296928
--- /dev/null
+++ b/pkg/invitations/transport/grpc/protobuf_type_converters.microgen.go
@@ -0,0 +1,161 @@
+// 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 (
+	"time"
+
+	service "git.perx.ru/perxis/perxis-go/pkg/invitations"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	pb "git.perx.ru/perxis/perxis-go/proto/invitations"
+	"github.com/golang/protobuf/ptypes"
+	timestamp "github.com/golang/protobuf/ptypes/timestamp"
+)
+
+func PtrTimeTimeToProto(validUntil *time.Time) (*timestamp.Timestamp, error) {
+	if validUntil == nil {
+		return nil, nil
+	}
+	t, err := ptypes.TimestampProto(*validUntil)
+	if err != nil {
+		return nil, err
+	}
+	return t, err
+}
+
+func ProtoToPtrTimeTime(protoValidUntil *timestamp.Timestamp) (*time.Time, error) {
+	if protoValidUntil == nil {
+		return nil, nil
+	}
+	t, err := ptypes.Timestamp(protoValidUntil)
+	if err != nil {
+		return nil, err
+	}
+	return &t, nil
+}
+
+func PtrInvitationToProto(invitation *service.Invitation) (*pb.Invitation, error) {
+	if invitation == nil {
+		return nil, nil
+	}
+	pi := &pb.Invitation{
+		Id:      invitation.ID,
+		Email:   invitation.Email,
+		OrgId:   invitation.OrgID,
+		SpaceId: invitation.SpaceID,
+		OwnerId: invitation.OwnerID,
+		Role:    invitation.Role,
+	}
+	if invitation.CreatedAt != nil && !invitation.CreatedAt.IsZero() {
+		t, _ := ptypes.TimestampProto(*invitation.CreatedAt)
+		pi.CreatedAt = t
+	}
+	if invitation.ValidUntil != nil && !invitation.ValidUntil.IsZero() {
+		t, _ := ptypes.TimestampProto(*invitation.ValidUntil)
+		pi.ValidUntil = t
+	}
+	return pi, nil
+}
+
+func ProtoToPtrInvitation(protoInvitation *pb.Invitation) (*service.Invitation, error) {
+	if protoInvitation == nil {
+		return nil, nil
+	}
+	i := &service.Invitation{
+		ID:      protoInvitation.Id,
+		Email:   protoInvitation.Email,
+		OrgID:   protoInvitation.OrgId,
+		SpaceID: protoInvitation.SpaceId,
+		OwnerID: protoInvitation.OwnerId,
+		Role:    protoInvitation.Role,
+	}
+	if protoInvitation.CreatedAt != nil {
+		t, _ := ptypes.Timestamp(protoInvitation.CreatedAt)
+		i.CreatedAt = &t
+	}
+	if protoInvitation.ValidUntil != nil {
+		t, _ := ptypes.Timestamp(protoInvitation.ValidUntil)
+		i.ValidUntil = &t
+	}
+	return i, nil
+}
+
+func PtrFilterToProto(filter *service.Filter) (*pb.Filter, error) {
+	if filter == nil {
+		return nil, nil
+	}
+	return &pb.Filter{
+		Id:      filter.ID,
+		Email:   filter.Email,
+		OrgId:   filter.OrgID,
+		SpaceId: filter.SpaceID,
+		OwnerId: filter.OwnerID,
+		Role:    filter.Role,
+	}, nil
+}
+
+func ProtoToPtrFilter(protoFilter *pb.Filter) (*service.Filter, error) {
+	if protoFilter == nil {
+		return nil, nil
+	}
+	return &service.Filter{
+		ID:      protoFilter.Id,
+		Email:   protoFilter.Email,
+		OrgID:   protoFilter.OrgId,
+		SpaceID: protoFilter.SpaceId,
+		OwnerID: protoFilter.OwnerId,
+		Role:    protoFilter.Role,
+	}, nil
+}
+
+func PtrServicesFindOptionsToProto(opts *options.FindOptions) (*pb.FindOptions, error) {
+	if opts == nil {
+		return nil, nil
+	}
+	return &pb.FindOptions{
+		Sort:     opts.Sort,
+		PageNum:  int32(opts.PageNum),
+		PageSize: int32(opts.PageSize),
+	}, nil
+}
+
+func ProtoToPtrServicesFindOptions(protoOpts *pb.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
+}
+
+func ListPtrInvitationToProto(invitations []*service.Invitation) ([]*pb.Invitation, error) {
+	protoInvitations := make([]*pb.Invitation, 0, len(invitations))
+	for _, i := range invitations {
+		pi, err := PtrInvitationToProto(i)
+		if err != nil {
+			return nil, err
+		}
+		protoInvitations = append(protoInvitations, pi)
+	}
+	return protoInvitations, nil
+}
+
+func ProtoToListPtrInvitation(protoInvitations []*pb.Invitation) ([]*service.Invitation, error) {
+	invitations := make([]*service.Invitation, 0, len(protoInvitations))
+	for _, pi := range protoInvitations {
+		p, err := ProtoToPtrInvitation(pi)
+		if err != nil {
+			return nil, err
+		}
+		invitations = append(invitations, p)
+	}
+	return invitations, nil
+}
diff --git a/pkg/invitations/transport/grpc/server.microgen.go b/pkg/invitations/transport/grpc/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..fa7cf241737173d3c41cadaa2ae9fa17ecb43fe7
--- /dev/null
+++ b/pkg/invitations/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/invitations/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/invitations"
+	grpc "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	context "golang.org/x/net/context"
+)
+
+type invitationsServer struct {
+	create grpc.Handler
+	get    grpc.Handler
+	accept grpc.Handler
+	find   grpc.Handler
+	delete grpc.Handler
+
+	pb.UnimplementedInvitationsServer
+}
+
+func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.InvitationsServer {
+	return &invitationsServer{
+		accept: grpc.NewServer(
+			endpoints.AcceptEndpoint,
+			_Decode_Accept_Request,
+			_Encode_Accept_Response,
+			opts...,
+		),
+		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...,
+		),
+	}
+}
+
+func (S *invitationsServer) 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 *invitationsServer) 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 *invitationsServer) Accept(ctx context.Context, req *pb.AcceptRequest) (*empty.Empty, error) {
+	_, resp, err := S.accept.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*empty.Empty), nil
+}
+
+func (S *invitationsServer) 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 *invitationsServer) 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/invitations/transport/server.microgen.go b/pkg/invitations/transport/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..326659f2b813d8f6b306d8b9498c0590c6434011
--- /dev/null
+++ b/pkg/invitations/transport/server.microgen.go
@@ -0,0 +1,63 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+
+	invitations "git.perx.ru/perxis/perxis-go/pkg/invitations"
+	endpoint "github.com/go-kit/kit/endpoint"
+)
+
+func Endpoints(svc invitations.Invitations) EndpointsSet {
+	return EndpointsSet{
+		AcceptEndpoint: AcceptEndpoint(svc),
+		CreateEndpoint: CreateEndpoint(svc),
+		DeleteEndpoint: DeleteEndpoint(svc),
+		FindEndpoint:   FindEndpoint(svc),
+		GetEndpoint:    GetEndpoint(svc),
+	}
+}
+
+func CreateEndpoint(svc invitations.Invitations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*CreateRequest)
+		res0, res1 := svc.Create(arg0, req.Invitation)
+		return &CreateResponse{Created: res0}, res1
+	}
+}
+
+func GetEndpoint(svc invitations.Invitations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetRequest)
+		res0, res1 := svc.Get(arg0, req.InvitationId)
+		return &GetResponse{Invitation: res0}, res1
+	}
+}
+
+func AcceptEndpoint(svc invitations.Invitations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*AcceptRequest)
+		res0 := svc.Accept(arg0, req.InvitationId, req.UserId)
+		return &AcceptResponse{}, res0
+	}
+}
+
+func FindEndpoint(svc invitations.Invitations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*FindRequest)
+		res0, res1, res2 := svc.Find(arg0, req.Filter, req.Opts)
+		return &FindResponse{
+			Invitations: res0,
+			Total:       res1,
+		}, res2
+	}
+}
+
+func DeleteEndpoint(svc invitations.Invitations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*DeleteRequest)
+		res0 := svc.Delete(arg0, req.InvitationId)
+		return &DeleteResponse{}, res0
+	}
+}
diff --git a/pkg/members/members.go b/pkg/members/members.go
new file mode 100644
index 0000000000000000000000000000000000000000..0993b6fddfe34cf7f257e6b70433893485a60dba
--- /dev/null
+++ b/pkg/members/members.go
@@ -0,0 +1,35 @@
+package members
+
+import (
+	"fmt"
+)
+
+type Member struct {
+	OrgId  string `bson:"orgId"`
+	UserId string `bson:"userId"`
+	Role   Role   `bson:"role"`
+}
+
+type Role uint
+
+const (
+	NotMember Role = iota
+	RoleMember
+	RoleOwner
+	RoleAdmin
+)
+
+func (r Role) IsPrivileged() bool {
+	return r == RoleOwner || r == RoleAdmin
+}
+
+func (r Role) Format(s fmt.State, verb rune) {
+	switch r {
+	case RoleOwner:
+		fmt.Fprint(s, "owner")
+	case RoleAdmin:
+		fmt.Fprint(s, "admin")
+	case RoleMember:
+		fmt.Fprint(s, "member")
+	}
+}
diff --git a/pkg/members/mocks/Members.go b/pkg/members/mocks/Members.go
new file mode 100644
index 0000000000000000000000000000000000000000..2adeaac705bf18a17eb06f7932d5d3769de1b50f
--- /dev/null
+++ b/pkg/members/mocks/Members.go
@@ -0,0 +1,124 @@
+// Code generated by mockery v2.7.4. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	members "git.perx.ru/perxis/perxis-go/pkg/members"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Members is an autogenerated mock type for the Members type
+type Members struct {
+	mock.Mock
+}
+
+// Get provides a mock function with given fields: ctx, orgId, userId
+func (_m *Members) Get(ctx context.Context, orgId string, userId string) (members.Role, error) {
+	ret := _m.Called(ctx, orgId, userId)
+
+	var r0 members.Role
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) members.Role); ok {
+		r0 = rf(ctx, orgId, userId)
+	} else {
+		r0 = ret.Get(0).(members.Role)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
+		r1 = rf(ctx, orgId, userId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// ListMembers provides a mock function with given fields: ctx, orgId
+func (_m *Members) ListMembers(ctx context.Context, orgId string) ([]*members.Member, error) {
+	ret := _m.Called(ctx, orgId)
+
+	var r0 []*members.Member
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*members.Member); ok {
+		r0 = rf(ctx, orgId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*members.Member)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, orgId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// ListOrganizations provides a mock function with given fields: ctx, userId
+func (_m *Members) ListOrganizations(ctx context.Context, userId string) ([]*members.Member, error) {
+	ret := _m.Called(ctx, userId)
+
+	var r0 []*members.Member
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*members.Member); ok {
+		r0 = rf(ctx, userId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*members.Member)
+		}
+	}
+
+	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
+}
+
+// Remove provides a mock function with given fields: ctx, orgId, userId
+func (_m *Members) Remove(ctx context.Context, orgId string, userId string) error {
+	ret := _m.Called(ctx, orgId, userId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, orgId, userId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// RemoveAll provides a mock function with given fields: ctx, orgId
+func (_m *Members) RemoveAll(ctx context.Context, orgId string) error {
+	ret := _m.Called(ctx, orgId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, orgId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Set provides a mock function with given fields: ctx, orgId, userId, role
+func (_m *Members) Set(ctx context.Context, orgId string, userId string, role members.Role) error {
+	ret := _m.Called(ctx, orgId, userId, role)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, members.Role) error); ok {
+		r0 = rf(ctx, orgId, userId, role)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
diff --git a/pkg/members/service.go b/pkg/members/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..5b88239b9b8509d8c1f65ca1a1a353cfe7281503
--- /dev/null
+++ b/pkg/members/service.go
@@ -0,0 +1,23 @@
+package members
+
+import (
+	"context"
+)
+
+// @microgen grpc
+// @protobuf git.perx.ru/perxis/perxis-go/proto/members
+// @grpc-addr account.members.Members
+type Members interface {
+	Set(ctx context.Context, orgId, userId string, role Role) (err error)
+
+	Get(ctx context.Context, orgId, userId string) (role Role, err error)
+
+	Remove(ctx context.Context, orgId, userId string) (err error)
+
+	// @microgen -
+	RemoveAll(ctx context.Context, orgId string) (err error)
+
+	ListMembers(ctx context.Context, orgId string) (members []*Member, err error)
+
+	ListOrganizations(ctx context.Context, userId string) (organizations []*Member, err error)
+}
diff --git a/pkg/members/transport/client.microgen.go b/pkg/members/transport/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..4cd10cad5b1b970f30a9380eae77a5d706748a08
--- /dev/null
+++ b/pkg/members/transport/client.microgen.go
@@ -0,0 +1,86 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	"errors"
+
+	members "git.perx.ru/perxis/perxis-go/pkg/members"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+func (set EndpointsSet) Set(arg0 context.Context, arg1 string, arg2 string, arg3 members.Role) (res0 error) {
+	request := SetRequest{
+		OrgId:  arg1,
+		Role:   arg3,
+		UserId: 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 members.Role, res1 error) {
+	request := GetRequest{
+		OrgId:  arg1,
+		UserId: 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{
+		OrgId:  arg1,
+		UserId: 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) RemoveAll(arg0 context.Context, arg1 string) (res0 error) {
+	return
+}
+
+func (set EndpointsSet) ListMembers(arg0 context.Context, arg1 string) (res0 []*members.Member, res1 error) {
+	request := ListMembersRequest{OrgId: arg1}
+	response, res1 := set.ListMembersEndpoint(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.(*ListMembersResponse).Members, res1
+}
+
+func (set EndpointsSet) ListOrganizations(arg0 context.Context, arg1 string) (res0 []*members.Member, res1 error) {
+	request := ListOrganizationsRequest{UserId: arg1}
+	response, res1 := set.ListOrganizationsEndpoint(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.(*ListOrganizationsResponse).Organizations, res1
+}
diff --git a/pkg/members/transport/endpoints.microgen.go b/pkg/members/transport/endpoints.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..88740bb07c63a46ff111527fdeae40dfc89a6167
--- /dev/null
+++ b/pkg/members/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 Members API and used for transport purposes.
+type EndpointsSet struct {
+	SetEndpoint               endpoint.Endpoint
+	GetEndpoint               endpoint.Endpoint
+	RemoveEndpoint            endpoint.Endpoint
+	ListMembersEndpoint       endpoint.Endpoint
+	ListOrganizationsEndpoint endpoint.Endpoint
+}
diff --git a/pkg/members/transport/exchanges.microgen.go b/pkg/members/transport/exchanges.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..afa03b65ed508022e89565e2f0881e6afb3c5678
--- /dev/null
+++ b/pkg/members/transport/exchanges.microgen.go
@@ -0,0 +1,44 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import members "git.perx.ru/perxis/perxis-go/pkg/members"
+
+type (
+	SetRequest struct {
+		OrgId  string       `json:"org_id"`
+		UserId string       `json:"user_id"`
+		Role   members.Role `json:"role"`
+	}
+	// Formal exchange type, please do not delete.
+	SetResponse struct{}
+
+	GetRequest struct {
+		OrgId  string `json:"org_id"`
+		UserId string `json:"user_id"`
+	}
+	GetResponse struct {
+		Role members.Role `json:"role"`
+	}
+
+	RemoveRequest struct {
+		OrgId  string `json:"org_id"`
+		UserId string `json:"user_id"`
+	}
+	// Formal exchange type, please do not delete.
+	RemoveResponse struct{}
+
+	ListMembersRequest struct {
+		OrgId string `json:"org_id"`
+	}
+	ListMembersResponse struct {
+		Members []*members.Member `json:"members"`
+	}
+
+	ListOrganizationsRequest struct {
+		UserId string `json:"user_id"`
+	}
+	ListOrganizationsResponse struct {
+		Organizations []*members.Member `json:"organizations"`
+	}
+)
diff --git a/pkg/members/transport/grpc/client.microgen.go b/pkg/members/transport/grpc/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..6df6b7951b031d7125b2a93174cf38117671ec7a
--- /dev/null
+++ b/pkg/members/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/members/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/members"
+	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.members.Members"
+	}
+	return transport.EndpointsSet{
+		GetEndpoint: grpckit.NewClient(
+			conn, addr, "Get",
+			_Encode_Get_Request,
+			_Decode_Get_Response,
+			pb.GetResponse{},
+			opts...,
+		).Endpoint(),
+		ListMembersEndpoint: grpckit.NewClient(
+			conn, addr, "ListMembers",
+			_Encode_ListMembers_Request,
+			_Decode_ListMembers_Response,
+			pb.ListMembersResponse{},
+			opts...,
+		).Endpoint(),
+		ListOrganizationsEndpoint: grpckit.NewClient(
+			conn, addr, "ListOrganizations",
+			_Encode_ListOrganizations_Request,
+			_Decode_ListOrganizations_Response,
+			pb.ListOrganizationsResponse{},
+			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/members/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/members/transport/grpc/protobuf_endpoint_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..9ee491a71335a89beb14582f02b8b26fd3a264d6
--- /dev/null
+++ b/pkg/members/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -0,0 +1,209 @@
+// 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/members/transport"
+	"git.perx.ru/perxis/perxis-go/proto/members"
+	"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)
+	reqRole, err := RoleToProto(req.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &members.SetRequest{
+		OrgId:  req.OrgId,
+		Role:   reqRole,
+		UserId: req.UserId,
+	}, 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 &members.GetRequest{
+		OrgId:  req.OrgId,
+		UserId: req.UserId,
+	}, 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 &members.RemoveRequest{
+		OrgId:  req.OrgId,
+		UserId: req.UserId,
+	}, nil
+}
+
+func _Encode_ListMembers_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListMembersRequest")
+	}
+	req := request.(*transport.ListMembersRequest)
+	return &members.ListMembersRequest{OrgId: req.OrgId}, nil
+}
+
+func _Encode_ListOrganizations_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListOrganizationsRequest")
+	}
+	req := request.(*transport.ListOrganizationsRequest)
+	return &members.ListOrganizationsRequest{UserId: req.UserId}, 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)
+	respRole, err := RoleToProto(resp.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &members.GetResponse{Role: respRole}, nil
+}
+
+func _Encode_Remove_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Encode_ListMembers_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListMembersResponse")
+	}
+	resp := response.(*transport.ListMembersResponse)
+	respMembers, err := ListPtrMemberToProto(resp.Members)
+	if err != nil {
+		return nil, err
+	}
+	return &members.ListMembersResponse{Members: respMembers}, nil
+}
+
+func _Encode_ListOrganizations_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListOrganizationsResponse")
+	}
+	resp := response.(*transport.ListOrganizationsResponse)
+	respOrganizations, err := ListPtrMemberToProto(resp.Organizations)
+	if err != nil {
+		return nil, err
+	}
+	return &members.ListOrganizationsResponse{Organizations: respOrganizations}, nil
+}
+
+func _Decode_Set_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil SetRequest")
+	}
+	req := request.(*members.SetRequest)
+	reqRole, err := ProtoToRole(req.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.SetRequest{
+		OrgId:  string(req.OrgId),
+		Role:   reqRole,
+		UserId: string(req.UserId),
+	}, nil
+}
+
+func _Decode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil GetRequest")
+	}
+	req := request.(*members.GetRequest)
+	return &transport.GetRequest{
+		OrgId:  string(req.OrgId),
+		UserId: string(req.UserId),
+	}, nil
+}
+
+func _Decode_Remove_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil RemoveRequest")
+	}
+	req := request.(*members.RemoveRequest)
+	return &transport.RemoveRequest{
+		OrgId:  string(req.OrgId),
+		UserId: string(req.UserId),
+	}, nil
+}
+
+func _Decode_ListMembers_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListMembersRequest")
+	}
+	req := request.(*members.ListMembersRequest)
+	return &transport.ListMembersRequest{OrgId: string(req.OrgId)}, nil
+}
+
+func _Decode_ListOrganizations_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil ListOrganizationsRequest")
+	}
+	req := request.(*members.ListOrganizationsRequest)
+	return &transport.ListOrganizationsRequest{UserId: string(req.UserId)}, 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.(*members.GetResponse)
+	respRole, err := ProtoToRole(resp.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.GetResponse{Role: respRole}, nil
+}
+
+func _Decode_Remove_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	return &empty.Empty{}, nil
+}
+
+func _Decode_ListMembers_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListMembersResponse")
+	}
+	resp := response.(*members.ListMembersResponse)
+	respMembers, err := ProtoToListPtrMember(resp.Members)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.ListMembersResponse{Members: respMembers}, nil
+}
+
+func _Decode_ListOrganizations_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil ListOrganizationsResponse")
+	}
+	resp := response.(*members.ListOrganizationsResponse)
+	respOrganizations, err := ProtoToListPtrMember(resp.Organizations)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.ListOrganizationsResponse{Organizations: respOrganizations}, nil
+}
diff --git a/pkg/members/transport/grpc/protobuf_type_converters.microgen.go b/pkg/members/transport/grpc/protobuf_type_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..70d4628a570042ecad26a9adc557f948a8098778
--- /dev/null
+++ b/pkg/members/transport/grpc/protobuf_type_converters.microgen.go
@@ -0,0 +1,42 @@
+// 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/members"
+	pb "git.perx.ru/perxis/perxis-go/proto/members"
+)
+
+func RoleToProto(role service.Role) (pb.Role, error) {
+	return pb.Role(role), nil
+}
+
+func ProtoToRole(protoRole pb.Role) (service.Role, error) {
+	return service.Role(protoRole), nil
+}
+
+func ListPtrMemberToProto(members []*service.Member) ([]*pb.Member, error) {
+	res := make([]*pb.Member, 0, len(members))
+	for _, m := range members {
+		res = append(res, &pb.Member{
+			OrgId:  m.OrgId,
+			UserId: m.UserId,
+			Role:   pb.Role(m.Role),
+		})
+	}
+	return res, nil
+}
+
+func ProtoToListPtrMember(protoMembers []*pb.Member) ([]*service.Member, error) {
+	res := make([]*service.Member, 0, len(protoMembers))
+	for _, m := range protoMembers {
+		res = append(res, &service.Member{
+			OrgId:  m.OrgId,
+			UserId: m.UserId,
+			Role:   service.Role(m.Role),
+		})
+	}
+	return res, nil
+}
diff --git a/pkg/members/transport/grpc/server.microgen.go b/pkg/members/transport/grpc/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..1d11e1c0990df144d99f511d20b4f844d4431b7a
--- /dev/null
+++ b/pkg/members/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/members/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/members"
+	grpc "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	context "golang.org/x/net/context"
+)
+
+type membersServer struct {
+	set               grpc.Handler
+	get               grpc.Handler
+	remove            grpc.Handler
+	listMembers       grpc.Handler
+	listOrganizations grpc.Handler
+
+	pb.UnimplementedMembersServer
+}
+
+func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.MembersServer {
+	return &membersServer{
+		get: grpc.NewServer(
+			endpoints.GetEndpoint,
+			_Decode_Get_Request,
+			_Encode_Get_Response,
+			opts...,
+		),
+		listMembers: grpc.NewServer(
+			endpoints.ListMembersEndpoint,
+			_Decode_ListMembers_Request,
+			_Encode_ListMembers_Response,
+			opts...,
+		),
+		listOrganizations: grpc.NewServer(
+			endpoints.ListOrganizationsEndpoint,
+			_Decode_ListOrganizations_Request,
+			_Encode_ListOrganizations_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 *membersServer) 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 *membersServer) 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 *membersServer) 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 *membersServer) ListMembers(ctx context.Context, req *pb.ListMembersRequest) (*pb.ListMembersResponse, error) {
+	_, resp, err := S.listMembers.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.ListMembersResponse), nil
+}
+
+func (S *membersServer) ListOrganizations(ctx context.Context, req *pb.ListOrganizationsRequest) (*pb.ListOrganizationsResponse, error) {
+	_, resp, err := S.listOrganizations.ServeGRPC(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return resp.(*pb.ListOrganizationsResponse), nil
+}
diff --git a/pkg/members/transport/server.microgen.go b/pkg/members/transport/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..b1c40ac3039b2ce46685880e93651f20d4fe7d16
--- /dev/null
+++ b/pkg/members/transport/server.microgen.go
@@ -0,0 +1,60 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+
+	members "git.perx.ru/perxis/perxis-go/pkg/members"
+	endpoint "github.com/go-kit/kit/endpoint"
+)
+
+func Endpoints(svc members.Members) EndpointsSet {
+	return EndpointsSet{
+		GetEndpoint:               GetEndpoint(svc),
+		ListMembersEndpoint:       ListMembersEndpoint(svc),
+		ListOrganizationsEndpoint: ListOrganizationsEndpoint(svc),
+		RemoveEndpoint:            RemoveEndpoint(svc),
+		SetEndpoint:               SetEndpoint(svc),
+	}
+}
+
+func SetEndpoint(svc members.Members) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*SetRequest)
+		res0 := svc.Set(arg0, req.OrgId, req.UserId, req.Role)
+		return &SetResponse{}, res0
+	}
+}
+
+func GetEndpoint(svc members.Members) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetRequest)
+		res0, res1 := svc.Get(arg0, req.OrgId, req.UserId)
+		return &GetResponse{Role: res0}, res1
+	}
+}
+
+func RemoveEndpoint(svc members.Members) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*RemoveRequest)
+		res0 := svc.Remove(arg0, req.OrgId, req.UserId)
+		return &RemoveResponse{}, res0
+	}
+}
+
+func ListMembersEndpoint(svc members.Members) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*ListMembersRequest)
+		res0, res1 := svc.ListMembers(arg0, req.OrgId)
+		return &ListMembersResponse{Members: res0}, res1
+	}
+}
+
+func ListOrganizationsEndpoint(svc members.Members) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*ListOrganizationsRequest)
+		res0, res1 := svc.ListOrganizations(arg0, req.UserId)
+		return &ListOrganizationsResponse{Organizations: res0}, res1
+	}
+}
diff --git a/pkg/organizations/mocks/Organizations.go b/pkg/organizations/mocks/Organizations.go
new file mode 100644
index 0000000000000000000000000000000000000000..7b7255897b5f33f44202ef8e79255a93ea7ee429
--- /dev/null
+++ b/pkg/organizations/mocks/Organizations.go
@@ -0,0 +1,120 @@
+// Code generated by mockery v2.7.4. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	organizations "git.perx.ru/perxis/perxis-go/pkg/organizations"
+	services "git.perx.ru/perxis/perxis-go/pkg/options"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Organizations is an autogenerated mock type for the Organizations type
+type Organizations struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, org
+func (_m *Organizations) Create(ctx context.Context, org *organizations.Organization) (*organizations.Organization, error) {
+	ret := _m.Called(ctx, org)
+
+	var r0 *organizations.Organization
+	if rf, ok := ret.Get(0).(func(context.Context, *organizations.Organization) *organizations.Organization); ok {
+		r0 = rf(ctx, org)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*organizations.Organization)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *organizations.Organization) error); ok {
+		r1 = rf(ctx, org)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, orgId
+func (_m *Organizations) Delete(ctx context.Context, orgId string) error {
+	ret := _m.Called(ctx, orgId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, orgId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Find provides a mock function with given fields: ctx, filter, opts
+func (_m *Organizations) Find(ctx context.Context, filter *organizations.Filter, opts *services.FindOptions) ([]*organizations.Organization, int, error) {
+	ret := _m.Called(ctx, filter, opts)
+
+	var r0 []*organizations.Organization
+	if rf, ok := ret.Get(0).(func(context.Context, *organizations.Filter, *services.FindOptions) []*organizations.Organization); ok {
+		r0 = rf(ctx, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*organizations.Organization)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *organizations.Filter, *services.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, *organizations.Filter, *services.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, orgId
+func (_m *Organizations) Get(ctx context.Context, orgId string) (*organizations.Organization, error) {
+	ret := _m.Called(ctx, orgId)
+
+	var r0 *organizations.Organization
+	if rf, ok := ret.Get(0).(func(context.Context, string) *organizations.Organization); ok {
+		r0 = rf(ctx, orgId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*organizations.Organization)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, orgId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Update provides a mock function with given fields: ctx, org
+func (_m *Organizations) Update(ctx context.Context, org *organizations.Organization) error {
+	ret := _m.Called(ctx, org)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *organizations.Organization) error); ok {
+		r0 = rf(ctx, org)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
diff --git a/pkg/organizations/organization.go b/pkg/organizations/organization.go
new file mode 100644
index 0000000000000000000000000000000000000000..b95daa6988710d26fcda200eebef8afeff9158df
--- /dev/null
+++ b/pkg/organizations/organization.go
@@ -0,0 +1,14 @@
+package organizations
+
+type Organization struct {
+	ID          string  `bson:"_id"`
+	Name        string  `bson:"name"`
+	Description string  `bson:"description"`
+	LogoURL     string  `bson:"logoUrl"`
+	OwnerID     *string `bson:"-"`
+}
+
+func (o *Organization) SetOwnerID(s string) *Organization {
+	o.OwnerID = &s
+	return o
+}
diff --git a/pkg/organizations/service.go b/pkg/organizations/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..9ba3b9a91e6acd2353b1cebff342fa6a329547c9
--- /dev/null
+++ b/pkg/organizations/service.go
@@ -0,0 +1,25 @@
+package organizations
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+// @microgen grpc
+// @protobuf git.perx.ru/perxis/perxis-go/proto/organizations
+// @grpc-addr account.organizations.Organizations
+type Organizations interface {
+	Create(ctx context.Context, org *Organization) (created *Organization, err error)
+
+	Get(ctx context.Context, orgId string) (org *Organization, err error)
+	Update(ctx context.Context, org *Organization) (err error)
+	Delete(ctx context.Context, orgId string) (err error)
+	Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (orgs []*Organization, total int, err error)
+}
+
+// Organizations
+type Filter struct {
+	ID   []string
+	Name []string
+}
diff --git a/pkg/organizations/transport/client.microgen.go b/pkg/organizations/transport/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..00b14af754877e02fe3936269d72cb13c7b138da
--- /dev/null
+++ b/pkg/organizations/transport/client.microgen.go
@@ -0,0 +1,76 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	"errors"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	organizations "git.perx.ru/perxis/perxis-go/pkg/organizations"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+func (set EndpointsSet) Create(arg0 context.Context, arg1 *organizations.Organization) (res0 *organizations.Organization, res1 error) {
+	request := CreateRequest{Org: 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) (res0 *organizations.Organization, res1 error) {
+	request := GetRequest{OrgId: 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).Org, res1
+}
+
+func (set EndpointsSet) Update(arg0 context.Context, arg1 *organizations.Organization) (res0 error) {
+	request := UpdateRequest{Org: 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{OrgId: 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) Find(arg0 context.Context, arg1 *organizations.Filter, arg2 *options.FindOptions) (res0 []*organizations.Organization, res1 int, res2 error) {
+	request := FindRequest{
+		Filter: arg1,
+		Opts:   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).Orgs, response.(*FindResponse).Total, res2
+}
diff --git a/pkg/organizations/transport/endpoints.microgen.go b/pkg/organizations/transport/endpoints.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..7e15457fef975b2f07611ef46786cf7b73f5bbee
--- /dev/null
+++ b/pkg/organizations/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 Organizations API and used for transport purposes.
+type EndpointsSet struct {
+	CreateEndpoint endpoint.Endpoint
+	GetEndpoint    endpoint.Endpoint
+	UpdateEndpoint endpoint.Endpoint
+	DeleteEndpoint endpoint.Endpoint
+	FindEndpoint   endpoint.Endpoint
+}
diff --git a/pkg/organizations/transport/exchanges.microgen.go b/pkg/organizations/transport/exchanges.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..7ae098e5b5b555cfe834c55e2fe73c79805fdbfd
--- /dev/null
+++ b/pkg/organizations/transport/exchanges.microgen.go
@@ -0,0 +1,45 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	organizations "git.perx.ru/perxis/perxis-go/pkg/organizations"
+)
+
+type (
+	CreateRequest struct {
+		Org *organizations.Organization `json:"org"`
+	}
+	CreateResponse struct {
+		Created *organizations.Organization `json:"created"`
+	}
+
+	GetRequest struct {
+		OrgId string `json:"org_id"`
+	}
+	GetResponse struct {
+		Org *organizations.Organization `json:"org"`
+	}
+
+	UpdateRequest struct {
+		Org *organizations.Organization `json:"org"`
+	}
+	// Formal exchange type, please do not delete.
+	UpdateResponse struct{}
+
+	DeleteRequest struct {
+		OrgId string `json:"org_id"`
+	}
+	// Formal exchange type, please do not delete.
+	DeleteResponse struct{}
+
+	FindRequest struct {
+		Filter *organizations.Filter `json:"filter"`
+		Opts   *options.FindOptions `json:"opts"`
+	}
+	FindResponse struct {
+		Orgs  []*organizations.Organization `json:"orgs"`
+		Total int                           `json:"total"`
+	}
+)
diff --git a/pkg/organizations/transport/grpc/client.microgen.go b/pkg/organizations/transport/grpc/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..92918a88884de0a621858b7e09c49c66d83e5cab
--- /dev/null
+++ b/pkg/organizations/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/organizations/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/organizations"
+	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.organizations.Organizations"
+	}
+	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(),
+		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/organizations/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/organizations/transport/grpc/protobuf_endpoint_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..966189093b756b558406ebacf758da6c2a3f4839
--- /dev/null
+++ b/pkg/organizations/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -0,0 +1,225 @@
+// 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/organizations/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/organizations"
+	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)
+	reqOrg, err := PtrOrganizationToProto(req.Org)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateRequest{Org: reqOrg}, 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{OrgId: req.OrgId}, 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{OrgId: req.OrgId}, 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
+	}
+	reqOpts, err := PtrServicesFindOptionsToProto(req.Opts)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.FindRequest{
+		Filter: reqFilter,
+		Opts:   reqOpts,
+	}, 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 := PtrOrganizationToProto(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateResponse{Created: respCreated}, 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)
+	respOrg, err := PtrOrganizationToProto(resp.Org)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.GetResponse{Org: respOrg}, 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_Find_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil FindResponse")
+	}
+	resp := response.(*transport.FindResponse)
+	respOrgs, err := ListPtrOrganizationToProto(resp.Orgs)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.FindResponse{
+		Orgs:  respOrgs,
+		Total: int64(resp.Total),
+	}, 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)
+	reqOrg, err := ProtoToPtrOrganization(req.Org)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateRequest{Org: reqOrg}, 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{OrgId: string(req.OrgId)}, 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{OrgId: string(req.OrgId)}, 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
+	}
+	reqOpts, err := ProtoToPtrServicesFindOptions(req.Opts)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.FindRequest{
+		Filter: reqFilter,
+		Opts:   reqOpts,
+	}, 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 := ProtoToPtrOrganization(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateResponse{Created: respCreated}, 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)
+	respOrg, err := ProtoToPtrOrganization(resp.Org)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.GetResponse{Org: respOrg}, 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_Find_Response(ctx context.Context, response interface{}) (interface{}, error) {
+	if response == nil {
+		return nil, errors.New("nil FindResponse")
+	}
+	resp := response.(*pb.FindResponse)
+	respOrgs, err := ProtoToListPtrOrganization(resp.Orgs)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.FindResponse{
+		Orgs:  respOrgs,
+		Total: int(resp.Total),
+	}, 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)
+	reqOrg, err := PtrOrganizationToProto(req.Org)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.UpdateRequest{Org: reqOrg}, 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)
+	reqOrg, err := ProtoToPtrOrganization(req.Org)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.UpdateRequest{Org: reqOrg}, nil
+}
diff --git a/pkg/organizations/transport/grpc/protobuf_type_converters.microgen.go b/pkg/organizations/transport/grpc/protobuf_type_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..15fce8f5a7966f8fa2958fef30f62805923f512b
--- /dev/null
+++ b/pkg/organizations/transport/grpc/protobuf_type_converters.microgen.go
@@ -0,0 +1,111 @@
+// 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 (
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"git.perx.ru/perxis/perxis-go/pkg/organizations"
+	"git.perx.ru/perxis/perxis-go/proto/common"
+	pb "git.perx.ru/perxis/perxis-go/proto/organizations"
+)
+
+func PtrOrganizationToProto(org *organizations.Organization) (*pb.Organization, error) {
+	if org == nil {
+		return nil, nil
+	}
+	po := &pb.Organization{
+		Id:          org.ID,
+		Name:        org.Name,
+		Description: org.Description,
+		LogoUrl:     org.LogoURL,
+		OwnerId:     org.OwnerID,
+	}
+
+	return po, nil
+}
+
+func ProtoToPtrOrganization(protoOrg *pb.Organization) (*organizations.Organization, error) {
+	if protoOrg == nil {
+		return nil, nil
+	}
+	o := &organizations.Organization{
+		ID:          protoOrg.Id,
+		Name:        protoOrg.Name,
+		Description: protoOrg.Description,
+		LogoURL:     protoOrg.LogoUrl,
+		OwnerID:     protoOrg.OwnerId,
+	}
+	return o, nil
+}
+
+func PtrFilterToProto(filter *organizations.Filter) (*pb.Filter, error) {
+	if filter == nil {
+		return nil, nil
+	}
+	return &pb.Filter{
+		Ids:   filter.ID,
+		Names: filter.Name,
+	}, nil
+}
+
+func ProtoToPtrFilter(protoFilter *pb.Filter) (*organizations.Filter, error) {
+	if protoFilter == nil {
+		return nil, nil
+	}
+	return &organizations.Filter{
+		ID:   protoFilter.Ids,
+		Name: protoFilter.Names,
+	}, 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
+}
+
+func ListPtrOrganizationToProto(orgs []*organizations.Organization) ([]*pb.Organization, error) {
+	protoOrgs := make([]*pb.Organization, 0, len(orgs))
+	for _, o := range orgs {
+		op, err := PtrOrganizationToProto(o)
+		if err != nil {
+			return nil, err
+		}
+		protoOrgs = append(protoOrgs, op)
+	}
+	return protoOrgs, nil
+}
+
+func ProtoToListPtrOrganization(protoOrgs []*pb.Organization) ([]*organizations.Organization, error) {
+	orgs := make([]*organizations.Organization, 0, len(protoOrgs))
+	for _, op := range protoOrgs {
+		o, err := ProtoToPtrOrganization(op)
+		if err != nil {
+			return nil, err
+		}
+		orgs = append(orgs, o)
+	}
+	return orgs, nil
+}
diff --git a/pkg/organizations/transport/grpc/server.microgen.go b/pkg/organizations/transport/grpc/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..1cf24d998b6422ad418cfbb0a47a51eba6fe801d
--- /dev/null
+++ b/pkg/organizations/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/organizations/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/organizations"
+	grpc "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	context "golang.org/x/net/context"
+)
+
+type organizationsServer struct {
+	create grpc.Handler
+	get    grpc.Handler
+	update grpc.Handler
+	delete grpc.Handler
+	find   grpc.Handler
+
+	pb.UnimplementedOrganizationsServer
+}
+
+func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.OrganizationsServer {
+	return &organizationsServer{
+		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...,
+		),
+		update: grpc.NewServer(
+			endpoints.UpdateEndpoint,
+			_Decode_Update_Request,
+			_Encode_Update_Response,
+			opts...,
+		),
+	}
+}
+
+func (S *organizationsServer) 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 *organizationsServer) 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 *organizationsServer) 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 *organizationsServer) 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 *organizationsServer) 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
+}
diff --git a/pkg/organizations/transport/server.microgen.go b/pkg/organizations/transport/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..2e08f8f534f862552ecc5008b2f76277e702a9ab
--- /dev/null
+++ b/pkg/organizations/transport/server.microgen.go
@@ -0,0 +1,63 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+
+	organizations "git.perx.ru/perxis/perxis-go/pkg/organizations"
+	endpoint "github.com/go-kit/kit/endpoint"
+)
+
+func Endpoints(svc organizations.Organizations) EndpointsSet {
+	return EndpointsSet{
+		CreateEndpoint: CreateEndpoint(svc),
+		DeleteEndpoint: DeleteEndpoint(svc),
+		FindEndpoint:   FindEndpoint(svc),
+		GetEndpoint:    GetEndpoint(svc),
+		UpdateEndpoint: UpdateEndpoint(svc),
+	}
+}
+
+func CreateEndpoint(svc organizations.Organizations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*CreateRequest)
+		res0, res1 := svc.Create(arg0, req.Org)
+		return &CreateResponse{Created: res0}, res1
+	}
+}
+
+func GetEndpoint(svc organizations.Organizations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetRequest)
+		res0, res1 := svc.Get(arg0, req.OrgId)
+		return &GetResponse{Org: res0}, res1
+	}
+}
+
+func UpdateEndpoint(svc organizations.Organizations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*UpdateRequest)
+		res0 := svc.Update(arg0, req.Org)
+		return &UpdateResponse{}, res0
+	}
+}
+
+func DeleteEndpoint(svc organizations.Organizations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*DeleteRequest)
+		res0 := svc.Delete(arg0, req.OrgId)
+		return &DeleteResponse{}, res0
+	}
+}
+
+func FindEndpoint(svc organizations.Organizations) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*FindRequest)
+		res0, res1, res2 := svc.Find(arg0, req.Filter, req.Opts)
+		return &FindResponse{
+			Orgs:  res0,
+			Total: res1,
+		}, res2
+	}
+}
diff --git a/pkg/roles/mocks/Roles.go b/pkg/roles/mocks/Roles.go
new file mode 100644
index 0000000000000000000000000000000000000000..d7e61236e36d2a5f544950f0634c5204d4a30a3e
--- /dev/null
+++ b/pkg/roles/mocks/Roles.go
@@ -0,0 +1,112 @@
+// Code generated by mockery v2.7.4. DO NOT EDIT.
+
+package mocks
+
+import (
+	"context"
+
+	roles "git.perx.ru/perxis/perxis-go/pkg/roles"
+	"github.com/stretchr/testify/mock"
+)
+
+// Roles is an autogenerated mock type for the Roles type
+type Roles struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, role
+func (_m *Roles) Create(ctx context.Context, role *roles.Role) (*roles.Role, error) {
+	ret := _m.Called(ctx, role)
+
+	var r0 *roles.Role
+	if rf, ok := ret.Get(0).(func(context.Context, *roles.Role) *roles.Role); ok {
+		r0 = rf(ctx, role)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*roles.Role)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *roles.Role) error); ok {
+		r1 = rf(ctx, role)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, spaceId, roleId
+func (_m *Roles) Delete(ctx context.Context, spaceId string, roleId string) error {
+	ret := _m.Called(ctx, spaceId, roleId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceId, roleId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Get provides a mock function with given fields: ctx, spaceId, roleId
+func (_m *Roles) Get(ctx context.Context, spaceId string, roleId string) (*roles.Role, error) {
+	ret := _m.Called(ctx, spaceId, roleId)
+
+	var r0 *roles.Role
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) *roles.Role); ok {
+		r0 = rf(ctx, spaceId, roleId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*roles.Role)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
+		r1 = rf(ctx, spaceId, roleId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// List provides a mock function with given fields: ctx, spaceId
+func (_m *Roles) List(ctx context.Context, spaceId string) ([]*roles.Role, error) {
+	ret := _m.Called(ctx, spaceId)
+
+	var r0 []*roles.Role
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*roles.Role); ok {
+		r0 = rf(ctx, spaceId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*roles.Role)
+		}
+	}
+
+	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, role
+func (_m *Roles) Update(ctx context.Context, role *roles.Role) error {
+	ret := _m.Called(ctx, role)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *roles.Role) error); ok {
+		r0 = rf(ctx, role)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
diff --git a/pkg/roles/role.go b/pkg/roles/role.go
new file mode 100644
index 0000000000000000000000000000000000000000..eecafb64135a8b4545fe7ef894529641ddbab69a
--- /dev/null
+++ b/pkg/roles/role.go
@@ -0,0 +1,73 @@
+package roles
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/data"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/permission"
+)
+
+const (
+	AnonymousRole  = "anonymous"
+	AuthorizedRole = "authorized"
+	ViewRole       = "view"
+)
+
+type Role struct {
+	// Внутренний идентификатор роли
+	ID string `json:"id" bson:"_id"`
+
+	// Идентификатор пространства
+	SpaceID string `json:"spaceId" bson:"-"`
+
+	// Описание роли, назначение
+	Description string `json:"description" bson:"description"`
+
+	// Список доступных окружений (ID или Alias)
+	Environments []string `json:"environments" bson:"environments"`
+
+	// Список правил доступа к коллекциям
+	Rules permission.Rules `json:"rules" bson:"rules"`
+
+	// Разрешить доступ API управления
+	AllowManagement bool `json:"allow_management" bson:"allow_management"`
+}
+
+func (r Role) CanAccessEnvironment(ctx context.Context, service environments.Environments, spaceID, envID string) bool {
+	if spaceID == "" || envID == "" {
+		return false
+	}
+
+	if r.AllowManagement {
+		return true
+	}
+
+	// Если явно не указаны доступные окружения - доступ по умолчанию к окружению master
+	if len(r.Environments) == 0 {
+		r.Environments = []string{environments.DefaultEnvironment}
+	}
+
+	for _, e := range r.Environments {
+		if envID == e || data.GlobMatch(envID, e) {
+			return true
+		}
+	}
+
+	env, err := service.Get(ctx, spaceID, envID)
+	if err != nil || env == nil {
+		return false
+	}
+
+	aliases := append(env.Aliases, env.ID)
+
+	for _, e := range r.Environments {
+		for _, a := range aliases {
+			if a == e || data.GlobMatch(a, e) {
+				return true
+			}
+		}
+	}
+
+	return false
+}
diff --git a/pkg/roles/service.go b/pkg/roles/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..b003008b10c7c0a0de6549c60262741fdf441f65
--- /dev/null
+++ b/pkg/roles/service.go
@@ -0,0 +1,16 @@
+package roles
+
+import (
+	"context"
+)
+
+// @microgen grpc
+// @protobuf git.perx.ru/perxis/perxis-go/proto/roles
+// @grpc-addr content.roles.Roles
+type Roles interface {
+	Create(ctx context.Context, role *Role) (created *Role, err error)
+	Get(ctx context.Context, spaceId, roleId string) (role *Role, err error)
+	List(ctx context.Context, spaceId string) (roles []*Role, err error)
+	Update(ctx context.Context, role *Role) (err error)
+	Delete(ctx context.Context, spaceId, roleId string) (err error)
+}
diff --git a/pkg/roles/transport/client.microgen.go b/pkg/roles/transport/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..71b1de9e2b1b746962effe61dc63c37cf7977e69
--- /dev/null
+++ b/pkg/roles/transport/client.microgen.go
@@ -0,0 +1,78 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+	"errors"
+
+	roles "git.perx.ru/perxis/perxis-go/pkg/roles"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+func (set EndpointsSet) Create(arg0 context.Context, arg1 *roles.Role) (res0 *roles.Role, res1 error) {
+	request := CreateRequest{Role: 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 *roles.Role, res1 error) {
+	request := GetRequest{
+		RoleId:  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).Role, res1
+}
+
+func (set EndpointsSet) List(arg0 context.Context, arg1 string) (res0 []*roles.Role, 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).Roles, res1
+}
+
+func (set EndpointsSet) Update(arg0 context.Context, arg1 *roles.Role) (res0 error) {
+	request := UpdateRequest{Role: 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{
+		RoleId:  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/roles/transport/endpoints.microgen.go b/pkg/roles/transport/endpoints.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..06fe337c3c48656eff8074237e5714c29eac8bb2
--- /dev/null
+++ b/pkg/roles/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 Roles API and used for transport purposes.
+type EndpointsSet struct {
+	CreateEndpoint endpoint.Endpoint
+	GetEndpoint    endpoint.Endpoint
+	ListEndpoint   endpoint.Endpoint
+	UpdateEndpoint endpoint.Endpoint
+	DeleteEndpoint endpoint.Endpoint
+}
diff --git a/pkg/roles/transport/exchanges.microgen.go b/pkg/roles/transport/exchanges.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..bb7d8e5010877efb628ce34b4f89ae66857476eb
--- /dev/null
+++ b/pkg/roles/transport/exchanges.microgen.go
@@ -0,0 +1,42 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import roles "git.perx.ru/perxis/perxis-go/pkg/roles"
+
+type (
+	CreateRequest struct {
+		Role *roles.Role `json:"role"`
+	}
+	CreateResponse struct {
+		Created *roles.Role `json:"created"`
+	}
+
+	GetRequest struct {
+		SpaceId string `json:"space_id"`
+		RoleId  string `json:"role_id"`
+	}
+	GetResponse struct {
+		Role *roles.Role `json:"role"`
+	}
+
+	ListRequest struct {
+		SpaceId string `json:"space_id"`
+	}
+	ListResponse struct {
+		Roles []*roles.Role `json:"roles"`
+	}
+
+	UpdateRequest struct {
+		Role *roles.Role `json:"role"`
+	}
+	// Formal exchange type, please do not delete.
+	UpdateResponse struct{}
+
+	DeleteRequest struct {
+		SpaceId string `json:"space_id"`
+		RoleId  string `json:"role_id"`
+	}
+	// Formal exchange type, please do not delete.
+	DeleteResponse struct{}
+)
diff --git a/pkg/roles/transport/grpc/client.microgen.go b/pkg/roles/transport/grpc/client.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..747b1a8cada8b5e189e5e4c7994c79cb36df8b02
--- /dev/null
+++ b/pkg/roles/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/roles/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/roles"
+	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.roles.Roles"
+	}
+	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(),
+		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/roles/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/roles/transport/grpc/protobuf_endpoint_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..aa66bf57105f290aae04a763c5a8e931a410fc53
--- /dev/null
+++ b/pkg/roles/transport/grpc/protobuf_endpoint_converters.microgen.go
@@ -0,0 +1,209 @@
+// 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/roles/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/roles"
+	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)
+	reqRole, err := PtrRoleToProto(req.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateRequest{Role: reqRole}, 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{
+		RoleId:  req.RoleId,
+		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_Update_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil UpdateRequest")
+	}
+	req := request.(*transport.UpdateRequest)
+	reqRole, err := PtrRoleToProto(req.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.UpdateRequest{Role: reqRole}, 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{
+		RoleId:  req.RoleId,
+		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)
+	respRole, err := PtrRoleToProto(resp.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.GetResponse{Role: respRole}, 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)
+	respRoles, err := ListPtrRoleToProto(resp.Roles)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.ListResponse{Roles: respRoles}, 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 _Decode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil CreateRequest")
+	}
+	req := request.(*pb.CreateRequest)
+	reqRole, err := ProtoToPtrRole(req.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateRequest{Role: reqRole}, 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{
+		RoleId:  string(req.RoleId),
+		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_Update_Request(ctx context.Context, request interface{}) (interface{}, error) {
+	if request == nil {
+		return nil, errors.New("nil UpdateRequest")
+	}
+	req := request.(*pb.UpdateRequest)
+	reqRole, err := ProtoToPtrRole(req.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.UpdateRequest{Role: reqRole}, 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{
+		RoleId:  string(req.RoleId),
+		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)
+	respRole, err := ProtoToPtrRole(resp.Role)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.GetResponse{Role: respRole}, 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)
+	respRoles, err := ProtoToListPtrRole(resp.Roles)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.ListResponse{Roles: respRoles}, 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 _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 := PtrRoleToProto(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &pb.CreateResponse{Created: respCreated}, 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 := ProtoToPtrRole(resp.Created)
+	if err != nil {
+		return nil, err
+	}
+	return &transport.CreateResponse{Created: respCreated}, nil
+}
diff --git a/pkg/roles/transport/grpc/protobuf_type_converters.microgen.go b/pkg/roles/transport/grpc/protobuf_type_converters.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..10f207e5ebf3e07804009bf0bbb62401e0bdbf9a
--- /dev/null
+++ b/pkg/roles/transport/grpc/protobuf_type_converters.microgen.go
@@ -0,0 +1,110 @@
+// 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 (
+	permission "git.perx.ru/perxis/perxis-go/pkg/permission"
+	service "git.perx.ru/perxis/perxis-go/pkg/roles"
+	commonpb "git.perx.ru/perxis/perxis-go/proto/common"
+	pb "git.perx.ru/perxis/perxis-go/proto/roles"
+)
+
+func PtrRoleToProto(role *service.Role) (*pb.Role, error) {
+	if role == nil {
+		return nil, nil
+	}
+	rules := make([]*commonpb.Rule, 0, len(role.Rules))
+	for _, r := range role.Rules {
+		pr, _ := PtrPermissionRuleToProto(r)
+		rules = append(rules, pr)
+	}
+	return &pb.Role{
+		Id:              role.ID,
+		SpaceId:         role.SpaceID,
+		Description:     role.Description,
+		Rules:           rules,
+		Environments:    role.Environments,
+		AllowManagement: role.AllowManagement,
+	}, nil
+}
+
+func ProtoToPtrRole(protoRole *pb.Role) (*service.Role, error) {
+	if protoRole == nil {
+		return nil, nil
+	}
+
+	rules := make([]*permission.Rule, 0, len(protoRole.Rules))
+	for _, pr := range protoRole.Rules {
+		r, _ := ProtoToPtrPermissionRule(pr)
+		rules = append(rules, r)
+	}
+
+	return &service.Role{
+		ID:              protoRole.Id,
+		SpaceID:         protoRole.SpaceId,
+		Description:     protoRole.Description,
+		Rules:           rules,
+		Environments:    protoRole.Environments,
+		AllowManagement: protoRole.AllowManagement,
+	}, nil
+}
+
+func ListPtrRoleToProto(roles []*service.Role) ([]*pb.Role, error) {
+	protoRoles := make([]*pb.Role, 0, len(roles))
+	for _, r := range roles {
+		protoRole, _ := PtrRoleToProto(r)
+		protoRoles = append(protoRoles, protoRole)
+	}
+	return protoRoles, nil
+}
+
+func ProtoToListPtrRole(protoRoles []*pb.Role) ([]*service.Role, error) {
+	roles := make([]*service.Role, 0, len(protoRoles))
+	for _, r := range protoRoles {
+		role, _ := ProtoToPtrRole(r)
+		roles = append(roles, role)
+	}
+	return roles, 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/roles/transport/grpc/server.microgen.go b/pkg/roles/transport/grpc/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..dc012c75f1ff3a6965d9152f204c7d7f6b61d285
--- /dev/null
+++ b/pkg/roles/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/roles/transport"
+	pb "git.perx.ru/perxis/perxis-go/proto/roles"
+	grpc "github.com/go-kit/kit/transport/grpc"
+	empty "github.com/golang/protobuf/ptypes/empty"
+	context "golang.org/x/net/context"
+)
+
+type rolesServer struct {
+	create grpc.Handler
+	get    grpc.Handler
+	list   grpc.Handler
+	update grpc.Handler
+	delete grpc.Handler
+
+	pb.UnimplementedRolesServer
+}
+
+func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.RolesServer {
+	return &rolesServer{
+		create: grpc.NewServer(
+			endpoints.CreateEndpoint,
+			_Decode_Create_Request,
+			_Encode_Create_Response,
+			opts...,
+		),
+		delete: grpc.NewServer(
+			endpoints.DeleteEndpoint,
+			_Decode_Delete_Request,
+			_Encode_Delete_Response,
+			opts...,
+		),
+		get: grpc.NewServer(
+			endpoints.GetEndpoint,
+			_Decode_Get_Request,
+			_Encode_Get_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 *rolesServer) 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 *rolesServer) 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 *rolesServer) 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 *rolesServer) 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 *rolesServer) 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/roles/transport/server.microgen.go b/pkg/roles/transport/server.microgen.go
new file mode 100644
index 0000000000000000000000000000000000000000..119aae999a13bab884d4f98d09346b1ef2d13071
--- /dev/null
+++ b/pkg/roles/transport/server.microgen.go
@@ -0,0 +1,60 @@
+// Code generated by microgen 0.9.1. DO NOT EDIT.
+
+package transport
+
+import (
+	"context"
+
+	roles "git.perx.ru/perxis/perxis-go/pkg/roles"
+	endpoint "github.com/go-kit/kit/endpoint"
+)
+
+func Endpoints(svc roles.Roles) EndpointsSet {
+	return EndpointsSet{
+		CreateEndpoint: CreateEndpoint(svc),
+		DeleteEndpoint: DeleteEndpoint(svc),
+		GetEndpoint:    GetEndpoint(svc),
+		ListEndpoint:   ListEndpoint(svc),
+		UpdateEndpoint: UpdateEndpoint(svc),
+	}
+}
+
+func CreateEndpoint(svc roles.Roles) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*CreateRequest)
+		res0, res1 := svc.Create(arg0, req.Role)
+		return &CreateResponse{Created: res0}, res1
+	}
+}
+
+func GetEndpoint(svc roles.Roles) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*GetRequest)
+		res0, res1 := svc.Get(arg0, req.SpaceId, req.RoleId)
+		return &GetResponse{Role: res0}, res1
+	}
+}
+
+func ListEndpoint(svc roles.Roles) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*ListRequest)
+		res0, res1 := svc.List(arg0, req.SpaceId)
+		return &ListResponse{Roles: res0}, res1
+	}
+}
+
+func UpdateEndpoint(svc roles.Roles) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*UpdateRequest)
+		res0 := svc.Update(arg0, req.Role)
+		return &UpdateResponse{}, res0
+	}
+}
+
+func DeleteEndpoint(svc roles.Roles) endpoint.Endpoint {
+	return func(arg0 context.Context, request interface{}) (interface{}, error) {
+		req := request.(*DeleteRequest)
+		res0 := svc.Delete(arg0, req.SpaceId, req.RoleId)
+		return &DeleteResponse{}, res0
+	}
+}
diff --git a/pkg/schema/field/array.go b/pkg/schema/field/array.go
new file mode 100644
index 0000000000000000000000000000000000000000..6700eb9b09af7bb714566dca8c82e90d026c7d57
--- /dev/null
+++ b/pkg/schema/field/array.go
@@ -0,0 +1,161 @@
+package field
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strconv"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"github.com/hashicorp/go-multierror"
+)
+
+var arrayType = &ArrayType{}
+
+type ArrayParameters struct {
+	Item *Field `json:"item"`
+}
+
+func (ArrayParameters) Type() Type { return arrayType }
+
+func (p ArrayParameters) Clone(reset bool) Parameters {
+	return &ArrayParameters{Item: p.Item.Clone(reset)}
+}
+
+type ArrayType struct{}
+
+func (ArrayType) Name() string {
+	return "array"
+}
+
+func (ArrayType) NewParameters() Parameters {
+	return &ArrayParameters{}
+}
+
+func (ArrayType) IsEmpty(v interface{}) bool {
+	arr, _ := v.([]interface{}) // todo: нужно возвращать ошибку?
+	return len(arr) == 0
+}
+
+//func (ArrayType) Decode(ctx *Context.Context, field *Field, v interface{}) (interface{}, error) {
+//	params, ok := field.Params.(*ArrayParameters)
+//	if !ok {
+//		return nil, errors.New("field parameters required")
+//	}
+//
+//	arr, ok := v.([]interface{})
+//	if !ok {
+//		return nil, fmt.Errorf("[]interface{} required")
+//	}
+//
+//	m := make([]interface{}, 0, len(arr))
+//
+//	for _, i := range arr {
+//		item, err := Decode(ctx, params.Item, i)
+//		if err != nil {
+//			return nil, err
+//		}
+//		m = append(m, item)
+//	}
+//
+//	return m, nil
+//}
+//
+//func (ArrayType) Encode(ctx *Context.Context, field *Field, v interface{}) (interface{}, error) {
+//	params, ok := field.Params.(*ArrayParameters)
+//	if !ok {
+//		return nil, errors.New("field parameters required")
+//	}
+//
+//	arr, ok := v.([]interface{})
+//	if !ok {
+//		return nil, fmt.Errorf("[]interface{} required")
+//	}
+//
+//	m := make([]interface{}, 0, len(arr))
+//
+//	for _, i := range arr {
+//		item, err := params.Item.Encode(ctx, i)
+//		if err != nil {
+//			return nil, err
+//		}
+//		m = append(m, item)
+//	}
+//
+//	return m, nil
+//}
+
+//func (ArrayType) Validate(ctx *Context.Context, field *Field, v interface{}) error {
+//	params, ok := field.Params.(*ArrayParameters)
+//	if !ok {
+//		return errors.New("field parameters required")
+//	}
+//
+//	m, ok := v.([]interface{})
+//	if !ok {
+//		return errors.New("[]interface{} is required")
+//	}
+//	for _, i := range m {
+//		err := params.Item.Validate(ctx, i)
+//		if err != nil {
+//			return err
+//		}
+//	}
+//	return nil
+//}
+
+func (ArrayType) Walk(ctx context.Context, field *Field, v interface{}, fn WalkFunc, opts *WalkOptions) (interface{}, bool, error) {
+	var changed bool
+	params, ok := field.Params.(*ArrayParameters)
+	if !ok {
+		return nil, false, errors.New("field parameters required")
+	}
+
+	// В массиве нет в данных и не выполняется обход по схеме
+	if !opts.WalkSchema && v == nil {
+		return nil, false, nil
+	}
+
+	// Выполняется обход по схеме
+	if opts.WalkSchema && v == nil {
+		params.Item.Walk(ctx, v, fn, WalkOpts(opts))
+		return nil, false, nil
+	}
+
+	arr, ok := v.([]interface{})
+	if !ok {
+		return nil, false, fmt.Errorf("incorrect type: \"%s\", expected \"[]interface{}\"", reflect.ValueOf(v).Kind())
+	}
+
+	m := make([]interface{}, 0, len(arr))
+
+	var merr *multierror.Error
+	for i, value := range arr {
+
+		valueNew, valueChanged, err := params.Item.Walk(ctx, value, fn, WalkOpts(opts))
+
+		if err != nil {
+			merr = multierror.Append(merr, errors.WithField(err, strconv.Itoa(i)))
+		}
+
+		if valueChanged {
+			m = append(m, valueNew)
+			changed = true
+		} else {
+			m = append(m, value)
+		}
+	}
+
+	if merr != nil {
+		merr.ErrorFormat = func(i []error) string {
+			return fmt.Sprintf("%d error(s)", len(i))
+		}
+		return nil, false, merr
+	}
+
+	return m, changed, nil
+}
+
+func Array(item *Field, o ...interface{}) *Field {
+	return NewField(&ArrayParameters{Item: item}, o...)
+}
diff --git a/pkg/schema/field/array_test.go b/pkg/schema/field/array_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..94e60258661932dc72c2c34c27ea7cecb5c6b33d
--- /dev/null
+++ b/pkg/schema/field/array_test.go
@@ -0,0 +1,85 @@
+package field
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestArrayField_Decode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{
+			"Correct",
+			Array(Number("float")),
+			[]interface{}{1.0, 2.0},
+			[]interface{}{1.0, 2.0},
+			false,
+		},
+		{
+			"Incorrect type",
+			Array(Number("int")),
+			"1 2 3",
+			"decode error: incorrect type: \"string\", expected \"[]interface{}\"",
+			true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Decode(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Decode() error = %v, want %v", err, tt.want.(string)))
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+				assert.ElementsMatch(t, got, tt.want, fmt.Sprintf("Decode() got = %v, want %v", got, tt.want))
+			}
+		})
+	}
+}
+
+func TestArrayField_Encode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{
+			"Correct",
+			Array(Number("float")),
+			[]interface{}{1.0, 2.0},
+			[]interface{}{1.0, 2.0},
+			false,
+		},
+		{
+			"Incorrect type",
+			Array(Number("int")),
+			"1 2 3",
+			"encode error: incorrect type: \"string\", expected \"[]interface{}\"",
+			true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Encode(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Decode() error = %v, want %v", err, tt.want.(string)))
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+				assert.ElementsMatch(t, got, tt.want, fmt.Sprintf("Decode() got = %v, want %v", got, tt.want))
+			}
+		})
+	}
+}
diff --git a/pkg/schema/field/boolean.go b/pkg/schema/field/boolean.go
new file mode 100644
index 0000000000000000000000000000000000000000..94580541f8e33f9930adad94bff6a5c18915d05b
--- /dev/null
+++ b/pkg/schema/field/boolean.go
@@ -0,0 +1,51 @@
+package field
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+)
+
+var boolType = &BoolType{}
+
+type BoolParameters struct{}
+
+func (b BoolParameters) Type() Type                   { return boolType }
+func (b *BoolParameters) Clone(reset bool) Parameters { return b }
+
+type BoolType struct{}
+
+func (b BoolType) Name() string {
+	return "bool"
+}
+
+func (b BoolType) NewParameters() Parameters {
+	return &BoolParameters{}
+}
+func (BoolType) IsEmpty(v interface{}) bool {
+	return v == nil
+}
+
+func (b BoolType) Decode(_ context.Context, field *Field, v interface{}) (interface{}, error) {
+	if v == nil {
+		return v, nil
+	}
+	if _, ok := v.(bool); ok {
+		return v, nil
+	}
+	return nil, fmt.Errorf("incorrect type: \"%s\", expected \"boolean\"", reflect.ValueOf(v).Kind())
+}
+
+func (b BoolType) Encode(_ context.Context, field *Field, v interface{}) (interface{}, error) {
+	if v == nil {
+		return v, nil
+	}
+	if _, ok := v.(bool); ok {
+		return v, nil
+	}
+	return nil, fmt.Errorf("incorrect type: \"%s\", expected \"boolean\"", reflect.ValueOf(v).Kind())
+}
+
+func Bool(o ...interface{}) *Field {
+	return NewField(&BoolParameters{}, o...)
+}
diff --git a/pkg/schema/field/boolean_test.go b/pkg/schema/field/boolean_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..aaa09dcec353d7648a976759bb4fcc98d0fe0d7a
--- /dev/null
+++ b/pkg/schema/field/boolean_test.go
@@ -0,0 +1,73 @@
+package field
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestBooleanField_Decode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct data bool", Bool(), true, true, false},
+		{"Correct data nil", Bool(), nil, nil, false},
+		{"Wrong data int", Bool(), 1, "decode error: incorrect type: \"int\", expected \"boolean\"", true},
+		{"Wrong data float", Bool(), 0.0, "decode error: incorrect type: \"float64\", expected \"boolean\"", true},
+		{"Wrong data array", Bool(), [2]bool{true}, "decode error: incorrect type: \"array\", expected \"boolean\"", true},
+		{"Wrong data slice", Bool(), []bool{true}, "decode error: incorrect type: \"slice\", expected \"boolean\"", true},
+		{"Wrong data map", Bool(), map[bool]int{true: 1}, "decode error: incorrect type: \"map\", expected \"boolean\"", true},
+		{"Wrong data string", Bool(), "2", "decode error: incorrect type: \"string\", expected \"boolean\"", true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Decode(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Decode() error = %v, want %v", err, tt.want.(string)))
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+				assert.Equal(t, got, tt.want, fmt.Sprintf("Decode() got = %v, want %v", got, tt.want))
+			}
+		})
+	}
+}
+
+func TestBooleanField_Encode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", Bool(), false, false, false},
+		{"Correct data nil", Bool(), nil, nil, false},
+		{"Wrong data int", Bool(), 1, "encode error: incorrect type: \"int\", expected \"boolean\"", true},
+		{"Wrong data float", Bool(), 0.0, "encode error: incorrect type: \"float64\", expected \"boolean\"", true},
+		{"Wrong data array", Bool(), [2]bool{true}, "encode error: incorrect type: \"array\", expected \"boolean\"", true},
+		{"Wrong data slice", Bool(), []bool{true}, "encode error: incorrect type: \"slice\", expected \"boolean\"", true},
+		{"Wrong data map", Bool(), map[bool]int{true: 1}, "encode error: incorrect type: \"map\", expected \"boolean\"", true},
+		{"Wrong data string", Bool(), "2", "encode error: incorrect type: \"string\", expected \"boolean\"", true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Encode(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Encode() error = %v, want %v", err, tt.want.(string)))
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+				assert.Equal(t, got, tt.want, fmt.Sprintf("Encode() got = %v, want %v", got, tt.want))
+			}
+		})
+	}
+}
diff --git a/pkg/schema/field/encode.go b/pkg/schema/field/encode.go
new file mode 100644
index 0000000000000000000000000000000000000000..d891b41228e4ff70142cff9bdf3bca8cc99506dd
--- /dev/null
+++ b/pkg/schema/field/encode.go
@@ -0,0 +1,66 @@
+package field
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+)
+
+type Decoder interface {
+	Decode(ctx context.Context, field *Field, v interface{}) (interface{}, error)
+}
+
+type Encoder interface {
+	Encode(ctx context.Context, field *Field, v interface{}) (interface{}, error)
+}
+
+func Decode(ctx context.Context, w Walker, v interface{}) (interface{}, error) {
+	var err error
+	//if ctx == nil {
+	//	ctx = NewContext()
+	//}
+	//
+	//if m, ok := v.(map[string]interface{}); ok {
+	//	ctx = ctx.ExtendEnv(m)
+	//	ctx.DisableConditions = true
+	//}
+
+	val, _, err := w.Walk(ctx, v, func(ctx context.Context, f *Field, v interface{}) (res WalkFuncResult, err error) {
+		if decoder, ok := f.GetType().(Decoder); ok {
+			if v, err = decoder.Decode(ctx, f, v); err != nil {
+				return
+			}
+			res.Value = v
+			res.Changed = true
+			return
+		}
+		res.Value = v
+		return
+	})
+
+	if err != nil {
+		return nil, errors.Wrap(err, "decode error")
+	}
+
+	return val, nil
+}
+
+func Encode(ctx context.Context, w Walker, v interface{}) (interface{}, error) {
+	val, _, err := w.Walk(ctx, v, func(ctx context.Context, f *Field, v interface{}) (res WalkFuncResult, err error) {
+		if encode, ok := f.GetType().(Encoder); ok {
+			if v, err = encode.Encode(ctx, f, v); err != nil {
+				return
+			}
+			res.Value = v
+			res.Changed = true
+			return
+		}
+		res.Value = v
+		return
+	})
+	if err != nil {
+		return nil, errors.Wrap(err, "encode error")
+	}
+	return val, nil
+
+}
diff --git a/pkg/schema/field/errors.go b/pkg/schema/field/errors.go
new file mode 100644
index 0000000000000000000000000000000000000000..8ee095285a7493cf177cd9185d7235a344c49e67
--- /dev/null
+++ b/pkg/schema/field/errors.go
@@ -0,0 +1,7 @@
+package field
+
+import "errors"
+
+var (
+	ErrSkipOption = errors.New("option invalid, skipped")
+)
diff --git a/pkg/schema/field/evaluate.go b/pkg/schema/field/evaluate.go
new file mode 100644
index 0000000000000000000000000000000000000000..ad273819651a434a93e810564c0de5444579035f
--- /dev/null
+++ b/pkg/schema/field/evaluate.go
@@ -0,0 +1,49 @@
+package field
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
+)
+
+const EvaluatePassesLimit = 10
+
+func Evaluate(ctx context.Context, w Walker, v interface{}) (interface{}, error) {
+	var err error
+
+	chg := true
+	val := v
+	i := 0
+
+	for chg {
+		m, _ := val.(map[string]interface{})
+
+		val, chg, err = w.Walk(expr.WithEnv(ctx, m), val, func(ctx context.Context, f *Field, v interface{}) (res WalkFuncResult, err error) {
+			enabled, _ := f.IsEnabled(ctx)
+
+			if !enabled {
+				res.Stop = true
+				if v != nil {
+					res.Changed = true // Значение изменилось на пустое
+				}
+				return
+			}
+
+			res.Value = v
+			return
+		})
+
+		if err != nil {
+			return nil, errors.Wrap(err, "evaluation error")
+		}
+
+		i += 1
+
+		if i > EvaluatePassesLimit {
+			return nil, errors.New("fail to evaluate data conditions")
+		}
+	}
+
+	return val, nil
+}
diff --git a/pkg/schema/field/field.go b/pkg/schema/field/field.go
new file mode 100644
index 0000000000000000000000000000000000000000..45ddb40d128ab3f87ff7c41e3cedf75009ba3f98
--- /dev/null
+++ b/pkg/schema/field/field.go
@@ -0,0 +1,528 @@
+package field
+
+import (
+	"context"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/data"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
+)
+
+const (
+	FieldSeparator = "."
+	IncludeLimit   = 10
+)
+
+type (
+	Preparer interface {
+		Prepare(f *Field) error
+	}
+
+	Fielder interface {
+		GetField(path string) *Field
+	}
+)
+
+type Translation struct {
+	Locale      string `json:"locale,omitempty"`
+	Title       string `json:"title,omitempty"`
+	Description string `json:"description,omitempty"`
+}
+
+type View struct {
+	Widget  string                 `json:"widget,omitempty"`  // Виджет для отображения поля в списке
+	Options map[string]interface{} `json:"options,omitempty"` // Опции виджета, на усмотрения виджета
+}
+
+type UI struct {
+	Widget      string                 `json:"widget,omitempty"`      // Имя виджета для отображения поля в пользовательском интерфейсе
+	Placeholder string                 `json:"placeholder,omitempty"` // Подсказка для заполнения значения
+	Options     map[string]interface{} `json:"options,omitempty"`     // Опции виджета для отображения
+	ReadView    *View                  `json:"read_view,omitempty"`   // Настройки для отображения экрана в режиме просмотра элемента
+	EditView    *View                  `json:"edit_view,omitempty"`   // Настройки для отображения экрана в режиме редактирования элемента
+	ListView    *View                  `json:"list_view,omitempty"`   // Настройки для отображения экрана в режиме списке элементов
+}
+
+type Include struct {
+	Ref      string `json:"ref,omitempty"`
+	Optional bool   `json:"optional,omitempty"`
+}
+
+type Field struct {
+	Title            string        `json:"title,omitempty"`             // Название поля (Например: name)
+	Description      string        `json:"description,omitempty"`       // Описание поле (Например: User name)
+	Translations     []Translation `json:"translations,omitempty"`      // Переводы данных на разных языках
+	UI               *UI           `json:"ui,omitempty"`                // Опции пользовательского интерфейса
+	Includes         []Include     `json:"includes,omitempty"`          // Импорт схем
+	SingleLocale     bool          `json:"singleLocale,omitempty"`      // Без перевода
+	Indexed          bool          `json:"indexed,omitempty"`           // Построить индекс для поля
+	Unique           bool          `json:"unique,omitempty"`            // Значение поля должны быть уникальными
+	TextSearch       bool          `json:"text_search,omitempty"`       // Значение поля доступны для полнотекстового поиска
+	Params           Parameters    `json:"-"`                           // Параметры поля, определяет так же тип поля
+	Options          Options       `json:"options,omitempty"`           // Дополнительные опции
+	Condition        string        `json:"condition,omitempty"`         // Условие отображения поля
+	AdditionalValues bool          `json:"additional_values,omitempty"` // Разрешает дополнительные значения вне ограничений правил
+
+	prepared bool
+}
+
+// TODO: Replace with Named field???
+type PathField struct {
+	Field
+	Name string
+	Path string
+}
+
+type NamedField struct {
+	*Field
+	Name string
+}
+
+func NewField(params Parameters, opts ...interface{}) *Field {
+	f := &Field{}
+	f.Params = params
+	f.Options.Add(opts...)
+	return f
+}
+
+func (f Field) GetType() Type {
+	return f.Params.Type()
+}
+
+func (f *Field) AddOptions(t ...interface{}) *Field {
+	f.Options.Add(t...)
+	return f
+}
+
+func (f Field) WithUI(ui *UI) *Field {
+	f.UI = ui
+	return &f
+}
+
+func (f *Field) SetIncludes(includes ...interface{}) {
+	f.Includes = make([]Include, 0, len(includes))
+	for _, i := range includes {
+		switch v := i.(type) {
+		case string:
+			f.Includes = append(f.Includes, Include{Ref: v})
+		case Include:
+			f.Includes = append(f.Includes, v)
+		default:
+			panic("incorrect import type")
+		}
+	}
+}
+
+func (f Field) WithIncludes(includes ...interface{}) *Field {
+	f.SetIncludes(includes...)
+	return &f
+}
+
+func (f Field) GetIncludes() []string {
+	return f.getIncludes()
+}
+
+func (f Field) getIncludes() []string {
+	res := make([]string, len(f.Includes))
+	for i, inc := range f.Includes {
+		res[i] = inc.Ref
+	}
+	nested := f.GetNestedFields()
+	for _, fld := range nested {
+		res = append(res, fld.getIncludes()...)
+	}
+	return res
+}
+
+func (f Field) IsIncluded(name string) bool {
+	return data.GlobMatch(name, f.GetIncludes()...)
+}
+
+func (f Field) SetTitle(title string) *Field {
+	f.Title = title
+	return &f
+}
+
+func (f Field) SetDescription(desc string) *Field {
+	f.Description = desc
+	return &f
+}
+
+func (f Field) AddTranslation(locale, title, desc string) *Field {
+	for i, t := range f.Translations {
+		if t.Locale == locale {
+			f.Translations[i] = Translation{Locale: locale, Title: title, Description: desc}
+			return &f
+		}
+	}
+
+	f.Translations = append(f.Translations, Translation{Locale: locale, Title: title, Description: desc})
+	return &f
+}
+
+func (f Field) SetSingleLocale(r bool) *Field {
+	f.SingleLocale = r
+	return &f
+}
+
+func (f Field) SetIndexed(r bool) *Field {
+	f.Indexed = r
+	return &f
+}
+
+func (f Field) SetAdditionalValues() *Field {
+	f.AdditionalValues = true
+	return &f
+}
+
+func (f Field) SetUnique(r bool) *Field {
+	f.Unique = r
+	return &f
+}
+
+func (f Field) SetTextSearch(r bool) *Field {
+	f.TextSearch = r
+	return &f
+}
+
+func (f Field) SetCondition(c string) *Field {
+	f.Condition = c
+	return &f
+}
+
+func (f *Field) MustEnabled(ctx context.Context) bool {
+	if enabled, err := f.IsEnabled(ctx); !enabled || err != nil {
+		return false
+	}
+	return true
+}
+
+func (f *Field) IsEnabled(ctx context.Context) (bool, error) {
+	if f.Condition != "" {
+		out, err := expr.Eval(ctx, f.Condition, nil)
+		if err != nil {
+			return false, err
+		}
+
+		if enabled, ok := out.(bool); ok {
+			return enabled, nil
+		}
+
+		return false, errors.New("condition returns non-boolean value")
+	}
+
+	return true, nil
+}
+
+// Walk - выполняет обход данных по схеме и выполняет функцию, которая может модифицировать данные при необходимости
+func (f *Field) Walk(ctx context.Context, v interface{}, fn WalkFunc, opt ...WalkOption) (interface{}, bool, error) {
+	res, err := fn(ctx, f, v)
+
+	if err != nil {
+		return nil, false, err
+	}
+
+	if res.Changed || res.Stop {
+		return res.Value, res.Changed, err
+	}
+
+	if res.Context != nil {
+		ctx = res.Context
+	}
+
+	if walker, ok := f.GetType().(FieldWalker); ok {
+		val, changed, err := walker.Walk(ctx, f, v, fn, NewWalkOptions(opt...))
+		if err != nil {
+			return nil, false, err
+		}
+		return val, changed, err
+	}
+
+	return v, false, nil
+}
+
+// DEPRECATED
+func (f *Field) Prepare() error {
+	if preparer, ok := f.GetType().(Preparer); ok {
+		if err := preparer.Prepare(f); err != nil {
+			return err
+		}
+	}
+	for _, o := range f.Options {
+		if preparer, ok := o.(Preparer); ok {
+			if err := preparer.Prepare(f); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// GetField возвращает поле по строковому пути
+func (f *Field) GetField(path string) *Field {
+	if path == "" {
+		switch params := f.Params.(type) {
+		case *ArrayParameters:
+			// Возвращаем поле Item если путь указан как "arr."
+			return params.Item
+		}
+		return nil
+	}
+
+	switch params := f.Params.(type) {
+	case *ObjectParameters:
+		pp := strings.SplitN(path, FieldSeparator, 2)
+
+		for k, v := range params.Fields {
+
+			p, ok := v.Params.(*ObjectParameters)
+			if ok && p.Inline {
+				f := v.GetField(path)
+				if f != nil {
+					return f
+				}
+			}
+
+			if k == pp[0] {
+				if len(pp) == 1 {
+					return v
+				}
+				return v.GetField(pp[1])
+			}
+		}
+	case Fielder:
+		return params.GetField(path)
+
+	case *ArrayParameters:
+		return params.Item.GetField(path)
+	}
+
+	return nil
+}
+
+// GetFieldsPath возвращает полный путь для массива полей
+func GetFieldsPath(flds []PathField) (res []string) {
+	for _, f := range flds {
+		res = append(res, f.Path)
+	}
+	return res
+}
+
+type FilterFunc func(*Field, string) bool
+
+func GetAll(field *Field, path string) bool { return true }
+
+func (f *Field) GetFields(filterFunc FilterFunc, pathPrefix ...string) (res []PathField) {
+	var path string
+
+	if len(pathPrefix) > 0 {
+		path = pathPrefix[0]
+	}
+
+	// добавление корневого объекта для чего-то нужно?
+	if path != "" && filterFunc(f, path) {
+		res = append(res, PathField{
+			Field: *f,
+			Path:  path,
+		})
+	}
+
+	switch params := f.Params.(type) {
+	case *ObjectParameters:
+		res = append(res, getFieldsObject(path, params, filterFunc, false)...)
+	case *ArrayParameters:
+		res = append(res, getFieldsArray(path, params, filterFunc)...)
+	}
+
+	//if len(pathPrefix) > 0 {
+	//	for _, r := range res {
+	//		r.Path = strings.Join([]string{pathPrefix[0], r.Path}, FieldSeparator)
+	//	}
+	//}
+
+	return res
+}
+
+func getFieldsArray(path string, params *ArrayParameters, filterFunc FilterFunc) (res []PathField) {
+
+	switch params := params.Item.Params.(type) {
+	case *ObjectParameters:
+		res = append(res, getFieldsObject(path, params, filterFunc, params.Inline)...)
+
+	case *ArrayParameters:
+		res = append(res, getFieldsArray(path, params, filterFunc)...)
+	}
+
+	return res
+}
+
+func getFieldsObject(path string, params *ObjectParameters, filterFunc FilterFunc, ignoreInline bool) (res []PathField) {
+	for k, v := range params.Fields {
+		if v == nil {
+			continue
+		}
+
+		var newPath string
+		lastIdx := strings.LastIndex(path, ".")
+
+		if path == "" || !ignoreInline && params.Inline && lastIdx < 0 {
+			newPath = k
+		} else {
+			if !params.Inline || ignoreInline {
+				newPath = strings.Join([]string{path, k}, FieldSeparator)
+			} else {
+				newPath = strings.Join([]string{path[:lastIdx], k}, FieldSeparator)
+			}
+		}
+
+		if flds := v.GetFields(filterFunc, newPath); len(flds) > 0 {
+			res = append(res, flds...)
+		}
+	}
+
+	return res
+}
+
+func (f *Field) GetNestedFields() []*Field {
+	switch params := f.Params.(type) {
+	case *ObjectParameters:
+		flds := make([]*Field, 0, len(params.Fields))
+		for _, v := range params.Fields {
+			if v == nil {
+				continue
+			}
+			flds = append(flds, v)
+		}
+		return flds
+	case *ArrayParameters:
+		return []*Field{params.Item}
+	}
+
+	return nil
+}
+
+// Clone создает копию поля
+// Параметр reset указывает необходимо ли отвязать параметры поля от вложенных полей
+func (f Field) Clone(reset bool) *Field {
+	if f.UI != nil {
+		ui := *f.UI
+		f.UI = &ui
+	}
+
+	if len(f.Translations) > 0 {
+		f.Translations = append(make([]Translation, 0, len(f.Translations)), f.Translations...)
+	}
+
+	if f.Options != nil {
+		opts := make(Options)
+		for k, v := range f.Options {
+			opts[k] = v
+		}
+		f.Options = opts
+	}
+
+	if f.Params != nil {
+		f.Params = f.Params.Clone(reset)
+	}
+
+	return &f
+}
+
+func (f *Field) mergeField(fld *Field) error {
+	if f.Title == "" {
+		f.Title = fld.Title
+	}
+
+	if f.Description == "" {
+		f.Description = fld.Description
+	}
+
+	if len(f.Translations) == 0 {
+		f.Translations = fld.Translations
+	}
+
+	if f.UI == nil {
+		f.UI = fld.UI
+	}
+
+	if len(f.Includes) > 0 {
+		f.Includes = fld.Includes
+	}
+
+	if f.Params == nil {
+		f.Params = fld.Params
+	} else if fld.Params != nil {
+		type Merger interface {
+			Merge(parameters Parameters) error
+		}
+
+		if merger, ok := f.Params.(Merger); ok {
+			if err := merger.Merge(fld.Params); err != nil {
+				return err
+			}
+		}
+	}
+
+	if f.Options == nil {
+		f.Options = fld.Options
+	}
+
+	if f.Condition == "" {
+		f.Condition = fld.Condition
+	}
+
+	return nil
+}
+
+func (f *Field) Merge(fields ...*Field) error {
+	for _, fld := range fields {
+		f.mergeField(fld)
+	}
+	return nil
+}
+
+func (f *Field) loadIncludes(ctx context.Context, loader Loader, depth int) error {
+	if depth > IncludeLimit {
+		return errors.New("limit for included fields exceeded")
+	}
+
+	for _, i := range f.Includes {
+		if loader == nil {
+			panic("schema loader not set")
+		}
+		importedField, err := loader.Load(ctx, i.Ref)
+		if err != nil {
+			if i.Optional {
+				continue
+			}
+			return err
+		}
+
+		for _, fld := range importedField {
+			depth += 1
+			if err := fld.loadIncludes(ctx, loader, depth); err != nil {
+				return err
+			}
+		}
+
+		if err = f.Merge(importedField...); err != nil {
+			return err
+		}
+	}
+	for _, i := range f.GetNestedFields() {
+		if err := i.loadIncludes(ctx, loader, depth); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (f *Field) LoadIncludes(ctx context.Context, loader Loader) error {
+	return f.loadIncludes(ctx, loader, 0)
+}
+
+func (f *Field) LoadRef(ctx context.Context, ref string, loader Loader) error {
+	f.SetIncludes(ref)
+	return f.LoadIncludes(ctx, loader)
+}
diff --git a/pkg/schema/field/field_json.go b/pkg/schema/field/field_json.go
new file mode 100644
index 0000000000000000000000000000000000000000..dfb212044838d66da90436b127594427d3f842da
--- /dev/null
+++ b/pkg/schema/field/field_json.go
@@ -0,0 +1,86 @@
+package field
+
+import (
+	"encoding/json"
+
+	jsoniter "github.com/json-iterator/go"
+	"github.com/pkg/errors"
+)
+
+type FieldData Field
+
+type jsonField struct {
+	FieldData
+	Type   string          `json:"type"`
+	Params json.RawMessage `json:"params,omitempty"`
+}
+
+type ParametersConverter interface {
+	ConvertParameters(p Parameters) (Parameters, error)
+}
+
+type ParametersValidator interface {
+	ValidateParameters(p Parameters) error
+}
+
+func (f *Field) UnmarshalJSON(b []byte) error {
+	var j jsonField
+	if err := jsoniter.Unmarshal(b, &j); err != nil {
+		return errors.Wrapf(err, "error unmarshal json into field")
+	}
+
+	typ, ok := GetType(j.Type)
+
+	jsonParams := j.Params
+
+	// Unknown type, создаем поля Unknown которое будет хранить оригинальные параметры
+	if !ok {
+		*f = Field(j.FieldData)
+		f.Params = &UnknownParameters{Typ: j.Type, Params: jsonParams}
+		return nil
+	}
+
+	params := typ.NewParameters()
+	if len(j.Params) > 0 {
+		if err := jsoniter.Unmarshal(jsonParams, params); err != nil {
+			return errors.Wrapf(err, "error unmarshal json into field type %s", typ.Name())
+		}
+	}
+
+	if converter, ok := typ.(ParametersConverter); ok {
+		var err error
+		if params, err = converter.ConvertParameters(params); err != nil {
+			return errors.Wrap(err, "error unmarshal json")
+		}
+	}
+
+	if validator, ok := typ.(ParametersValidator); ok {
+		var err error
+		if err = validator.ValidateParameters(params); err != nil {
+			return errors.Wrap(err, "error validate json")
+		}
+	}
+
+	*f = Field(j.FieldData)
+	f.Params = params
+	f.Prepare()
+	return nil
+}
+
+func (f *Field) MarshalJSON() ([]byte, error) {
+	j := jsonField{
+		FieldData: FieldData(*f),
+	}
+
+	j.Type = f.GetType().Name()
+
+	if f.Params != nil {
+		b, err := jsoniter.Marshal(f.Params)
+		if err != nil {
+			return nil, err
+		}
+		j.Params = b
+	}
+
+	return jsoniter.Marshal(&j)
+}
diff --git a/pkg/schema/field/init.go b/pkg/schema/field/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..45c4d11c1ecdc51927109cb3202704e093c2d53d
--- /dev/null
+++ b/pkg/schema/field/init.go
@@ -0,0 +1,13 @@
+package field
+
+func init() {
+	Register(boolType)
+	Register(stringType)
+	Register(numberType)
+	Register(arrayType)
+	Register(timeType)
+	Register(objectType)
+	Register(unknownType)
+	Register(locationType)
+	Register(primaryKeyType)
+}
diff --git a/pkg/schema/field/loader.go b/pkg/schema/field/loader.go
new file mode 100644
index 0000000000000000000000000000000000000000..583ecc2c7c6bdc0d278819d2327280ec87858117
--- /dev/null
+++ b/pkg/schema/field/loader.go
@@ -0,0 +1,34 @@
+package field
+
+import (
+	"context"
+	"fmt"
+)
+
+// Loader интерфейс загрузчика схем
+type Loader interface {
+	Load(ctx context.Context, ref string) (fs []*Field, err error)
+}
+
+type LoaderFunc func(ref string) (fs []*Field, err error)
+
+func (f LoaderFunc) Load(ctx context.Context, ref string) (fs []*Field, err error) {
+	return f(ref)
+}
+
+type multiLoader struct {
+	loaders []Loader
+}
+
+func (c *multiLoader) Load(ctx context.Context, ref string) (fs []*Field, err error) {
+	for _, l := range c.loaders {
+		if f, err := l.Load(nil, ref); err == nil {
+			return f, nil
+		}
+	}
+	return nil, fmt.Errorf("invalid schema reference: %s", ref)
+}
+
+func MultiLoader(loaders ...Loader) Loader {
+	return &multiLoader{loaders: loaders}
+}
diff --git a/pkg/schema/field/location.go b/pkg/schema/field/location.go
new file mode 100644
index 0000000000000000000000000000000000000000..eb6e175552a2504b667658c8b227bc3b7ff661b8
--- /dev/null
+++ b/pkg/schema/field/location.go
@@ -0,0 +1,137 @@
+package field
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"github.com/mitchellh/mapstructure"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/options"
+	"go.mongodb.org/mongo-driver/x/bsonx"
+)
+
+var locationType = &LocationType{}
+
+type LocationParameters struct{}
+
+func (p LocationParameters) Type() Type                  { return locationType }
+func (p LocationParameters) Clone(reset bool) Parameters { return &LocationParameters{} }
+
+func (p LocationParameters) GetMongoIndexes(path string, f *Field) []mongo.IndexModel {
+	var add, geo mongo.IndexModel
+	a := path + ".address"
+	g := path + ".geometry"
+	add.Options = options.Index().SetName(a)
+	//add.Options.SetSparse(true)
+	add.Options.SetPartialFilterExpression(bson.M{a: bson.M{"$exists": true}})
+	geo.Options = options.Index().SetName(g)
+	if f.Unique {
+		add.Options.SetUnique(true)
+		geo.Options.SetUnique(true)
+	}
+
+	if f.Indexed {
+		add.Keys = bsonx.Doc{{Key: a, Value: bsonx.Int32(1)}}
+		geo.Keys = bsonx.Doc{{Key: g, Value: bsonx.String("2dsphere")}}
+	}
+	return []mongo.IndexModel{add, geo}
+}
+
+type LocationType struct{}
+
+type GeoJSON struct {
+	Type        string    `json:"type" bson:"type" mapstructure:"type,omitempty"`
+	Coordinates []float64 `json:"coordinates" bson:"coordinates" mapstructure:"coordinates"`
+}
+
+type GeoObject struct {
+	Address  string   `json:"address,omitempty" bson:"address" mapstructure:"address,omitempty"`
+	Geometry *GeoJSON `json:"geometry,omitempty" bson:"geometry" mapstructure:"geometry,omitempty"`
+}
+
+func (LocationType) Name() string {
+	return "location"
+}
+
+func (LocationType) NewParameters() Parameters {
+	return &LocationParameters{}
+}
+
+func (LocationType) IsEmpty(v interface{}) bool {
+	loc, _ := v.(*GeoObject)
+	return loc == nil || loc.Address != "" && loc.Geometry != nil
+}
+
+func (LocationType) Decode(_ context.Context, _ *Field, v interface{}) (interface{}, error) {
+
+	if v == nil {
+		return nil, nil
+	}
+
+	var g GeoObject
+	if err := mapstructure.Decode(v, &g); err != nil {
+		return nil, err
+	}
+
+	if g.Address == "" && g.Geometry == nil {
+		return nil, errors.New("address or coordinates required")
+	}
+
+	if g.Geometry != nil {
+		if len(g.Geometry.Coordinates) != 2 {
+			return nil, errors.New("latitude and longitude required")
+		}
+
+		lat := g.Geometry.Coordinates[0]
+		lon := g.Geometry.Coordinates[1]
+
+		if lat < -180 || lat > 180 {
+			return nil, errors.New("invalid longitude values, valid are between -180 and 180")
+		}
+
+		if lon < -90 || lon > 90 {
+			return nil, errors.New("invalid latitude values, valid are between -90 and 90")
+		}
+
+		if g.Geometry.Type != "Point" {
+			g.Geometry.Type = "Point"
+		}
+	}
+
+	return &g, nil
+}
+
+func (LocationType) Encode(_ context.Context, _ *Field, v interface{}) (interface{}, error) {
+
+	if v == nil {
+		return nil, nil
+	}
+
+	g, ok := v.(*GeoObject)
+	if !ok {
+		return nil, errors.New("couldn't encode GeoObject")
+	}
+
+	res := make(map[string]interface{})
+	if g.Address != "" {
+		res["address"] = g.Address
+	}
+
+	if g.Geometry != nil {
+		if len(g.Geometry.Coordinates) != 2 {
+			return nil, errors.New("latitude and longitude required")
+		}
+
+		lat := g.Geometry.Coordinates[0]
+		lon := g.Geometry.Coordinates[1]
+
+		res["geometry"] = map[string]interface{}{"type": g.Geometry.Type, "coordinates": []interface{}{lat, lon}}
+	}
+
+	return res, nil
+}
+
+func Location(o ...interface{}) *Field {
+	return NewField(&LocationParameters{}, o...)
+}
diff --git a/pkg/schema/field/location_test.go b/pkg/schema/field/location_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..5b8206b6128ae7f4bf8f4042cedc0540bf357992
--- /dev/null
+++ b/pkg/schema/field/location_test.go
@@ -0,0 +1,209 @@
+package field
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestLocationField_Decode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct",
+			Location(),
+			map[string]interface{}{
+				"address":  "msk",
+				"geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}},
+			},
+			&GeoObject{"msk", &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}},
+			false},
+		{"Correct",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}},
+			},
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}},
+			false},
+		{"Correct",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []interface{}{55.7042351, 37.6152822}},
+			},
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}},
+			false},
+		{"Correct",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []float64{55.7042351, 37.6152822}},
+			},
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}},
+			false},
+		{"Correct",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []interface{}{55, 37}},
+			},
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{55, 37}}},
+			false},
+		{"Correct",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []interface{}{180, 90}},
+			},
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{180, 90}}},
+			false},
+		{"Correct",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []interface{}{-180, -90}},
+			},
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{-180, -90}}},
+			false},
+		{"Correct",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []int{55, 37}},
+			},
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{55, 37}}},
+			false},
+		{"Correct",
+			Location(),
+			map[string]interface{}{
+				"address": "msk",
+			},
+			&GeoObject{Address: "msk"},
+			false},
+		{"Correct", Location(), nil, nil, false},
+
+		{"Wrong data", Location(), "", nil, true},
+		{"Wrong data", Location(), []interface{}{"55.7042351", "37.6152822"}, nil, true},
+		{"Wrong data", Location(), map[string]interface{}{"type": "Point", "coordinates": [][]interface{}{{55.7042351, 37.6152822}}}, nil, true},
+		{"Wrong data", Location(), []interface{}{55.7042351, 37.6152822, 1.0}, nil, true},
+		{"Wrong data",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []int{55, 37, 67}},
+			},
+			nil,
+			true},
+		{"Wrong data",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []interface{}{180}},
+			},
+			nil,
+			true},
+		{"Wrong data",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []interface{}{-180, -90.1}},
+			},
+			nil,
+			true},
+		{"Wrong data",
+			Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"coordinates": []interface{}{180.1, 90.1}},
+			},
+			nil,
+			true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Decode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Decode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestLocationField_Encode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", Location(),
+			&GeoObject{Address: "msk", Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}},
+			map[string]interface{}{
+				"address":  "msk",
+				"geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}},
+			false},
+		{"Correct", Location(),
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}},
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}},
+			false},
+		{"Correct", Location(),
+			&GeoObject{Address: "msk"},
+			map[string]interface{}{
+				"address": "msk"},
+			false},
+		{"Correct", Location(),
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{55, 37}}},
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.0, 37.0}}},
+			false},
+		{"Correct", Location(),
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{180, 90}}},
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{180.0, 90.0}}},
+			false},
+		{"Correct", Location(),
+			&GeoObject{Geometry: &GeoJSON{"Point", []float64{-180, -90}}},
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{-180.0, -90.0}}},
+			false},
+		{"Correct", Location(), nil, nil, false},
+		{"Correct", Location(),
+			&GeoObject{},
+			map[string]interface{}{},
+			false},
+
+		{"Wrong data", Location(), "", nil, true},
+		{"Wrong data", Location(), []interface{}{55.7042351, 37.6152822}, nil, true},
+		{"Wrong data", Location(),
+			map[string]interface{}{
+				"address":  "msk",
+				"geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}},
+			nil,
+			true},
+		{"Wrong data", Location(),
+			map[string]interface{}{
+				"geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}},
+			nil,
+			true},
+		{"Wrong data", Location(),
+			map[string]interface{}{
+				"address": "msk"},
+			nil,
+			true},
+		{"Wrong data", Location(), &GeoJSON{}, nil, true},
+		{"Wrong data", Location(), &GeoJSON{Coordinates: []float64{55.7042351, 37.6152822}}, nil, true},
+		{"Wrong data", Location(), &GeoObject{Geometry: &GeoJSON{"Point", []float64{-180, -90, 50}}}, nil, true},
+		{"Wrong data", Location(), &GeoObject{Geometry: &GeoJSON{"Point", []float64{-180}}}, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Encode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Encode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Encode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/field/number.go b/pkg/schema/field/number.go
new file mode 100644
index 0000000000000000000000000000000000000000..502a2fef7c406b4b4dc96aad34efce2e518a1e28
--- /dev/null
+++ b/pkg/schema/field/number.go
@@ -0,0 +1,112 @@
+package field
+
+import (
+	"context"
+	"math"
+
+	"github.com/pkg/errors"
+)
+
+const (
+	NumberFormatInt   = "int"
+	NumberFormatFloat = "float"
+)
+
+var numberType = &NumberType{}
+
+type NumberParameters struct {
+	Format string `json:"format,omitempty"`
+}
+
+func (NumberParameters) Type() Type                    { return numberType }
+func (p NumberParameters) Clone(reset bool) Parameters { return &p }
+
+type NumberType struct{}
+
+func (NumberType) Name() string {
+	return "number"
+}
+
+func (NumberType) NewParameters() Parameters {
+	return &NumberParameters{}
+}
+
+func (NumberType) IsEmpty(v interface{}) bool {
+	return v == nil
+}
+
+func ToNumber(i interface{}) (interface{}, error) {
+	switch v := i.(type) {
+	case int64:
+		return v, nil
+	case int:
+		return int64(v), nil
+	case int8:
+		return int64(v), nil
+	case int32:
+		return int64(v), nil
+	case uint64:
+		return v, nil
+	case uint:
+		return uint64(v), nil
+	case uint8:
+		return uint64(v), nil
+	case uint32:
+		return uint64(v), nil
+	case float32:
+		return float64(v), nil
+	case float64:
+		return v, nil
+	}
+	return 0, errors.Errorf("error convert \"%s\" to number", i)
+}
+
+func (n NumberType) Decode(ctx context.Context, field *Field, v interface{}) (interface{}, error) {
+	return n.decode(ctx, field, v)
+}
+
+func (NumberType) decode(_ context.Context, field *Field, v interface{}) (interface{}, error) {
+	params, ok := field.Params.(*NumberParameters)
+	if !ok {
+		return nil, errors.New("field parameters required")
+	}
+
+	if v == nil {
+		return v, nil
+	}
+
+	n, err := ToNumber(v)
+	if err != nil {
+		return nil, err
+	}
+
+	switch params.Format {
+	case NumberFormatInt:
+		switch i := n.(type) {
+		case int64:
+			return i, nil
+		case uint64:
+			return i, nil
+		case float64:
+			return int64(math.Round(i)), nil
+		}
+	case NumberFormatFloat:
+		switch i := n.(type) {
+		case float64:
+			return i, nil
+		case int64:
+			return float64(i), nil
+		case uint64:
+			return float64(i), nil
+		}
+	}
+	return n, nil
+}
+
+func (n NumberType) Encode(ctx context.Context, field *Field, v interface{}) (interface{}, error) {
+	return n.decode(ctx, field, v)
+}
+
+func Number(format string, o ...interface{}) *Field {
+	return NewField(&NumberParameters{Format: format}, o...)
+}
diff --git a/pkg/schema/field/number_test.go b/pkg/schema/field/number_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e731793d12135127dbfa67d949572f52708075a6
--- /dev/null
+++ b/pkg/schema/field/number_test.go
@@ -0,0 +1,88 @@
+package field
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestNumberField_Decode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", Number("int"), int64(2), int64(2), false},     // #0
+		{"Correct", Number("int"), 2.2, int64(2), false},          // #1
+		{"Correct", Number("int"), 2, int64(2), false},            // #2
+		{"Correct", Number("int"), float32(2.2), int64(2), false}, // #3
+		{"Correct", Number("int"), float64(2.6), int64(3), false}, // #4
+		{"Correct", Number("int"), 2.6, int64(3), false},          // #5
+
+		{"Correct", Number("float"), int8(2), 2.0, false},                    // #6
+		{"Correct", Number("float"), 2.2, 2.2, false},                        // #7
+		{"Correct", Number("float"), 2, 2.0, false},                          // #8
+		{"Correct", Number("float"), float32(2.2), 2.200000047683716, false}, // #9
+		{"Correct", Number("float"), int64(2), 2.0, false},                   // #10
+
+		{"Wrong data", Number("int"), "", nil, true},         // #0
+		{"Wrong data", Number("int"), []byte(""), nil, true}, // #1
+
+		{"Wrong data", Number("float"), "", nil, true},         // #2
+		{"Wrong data", Number("float"), []byte(""), nil, true}, // #3
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Decode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Decode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestNumberField_Encode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", Number("int"), int64(2), int64(2), false},     // #0
+		{"Correct", Number("int"), 2.2, int64(2), false},          // #1
+		{"Correct", Number("int"), 2, int64(2), false},            // #2
+		{"Correct", Number("int"), float32(2.2), int64(2), false}, // #3
+		{"Correct", Number("int"), float32(2.6), int64(3), false}, // #4
+		{"Correct", Number("int"), 2.6, int64(3), false},          // #5
+
+		{"Correct", Number("float"), int8(2), 2.0, false},                    // #6
+		{"Correct", Number("float"), 2.2, 2.2, false},                        // #7
+		{"Correct", Number("float"), 2, 2.0, false},                          // #8
+		{"Correct", Number("float"), float32(2.2), 2.200000047683716, false}, // #9
+		{"Correct", Number("float"), int64(2), 2.0, false},                   // #10
+
+		{"Wrong data", Number("int"), "", nil, true},         // #0
+		{"Wrong data", Number("int"), []byte(""), nil, true}, // #1
+
+		{"Wrong data", Number("float"), "", nil, true},         // #2
+		{"Wrong data", Number("float"), []byte(""), nil, true}, // #3
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Encode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Decode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/field/object.go b/pkg/schema/field/object.go
new file mode 100644
index 0000000000000000000000000000000000000000..d86aa2352544f68ea837152ec6799677f4624d1a
--- /dev/null
+++ b/pkg/schema/field/object.go
@@ -0,0 +1,297 @@
+package field
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"regexp"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
+	"github.com/hashicorp/go-multierror"
+)
+
+var objectType = &ObjectType{}
+var isValidName = regexp.MustCompile(`^[a-zA-Z][\w]*$`).MatchString
+
+type ObjectParameters struct {
+	Inline bool              `json:"inline"`
+	Fields map[string]*Field `json:"fields"`
+}
+
+func (ObjectParameters) Type() Type { return objectType }
+
+func (p ObjectParameters) Clone(reset bool) Parameters {
+	if reset {
+		p.Fields = nil
+		return &p
+	}
+
+	flds := make(map[string]*Field)
+	for k, v := range p.Fields {
+		flds[k] = v.Clone(reset)
+	}
+
+	p.Fields = flds
+	return &p
+}
+
+// IsInlineObject определяет являться ли поле name инлайн объектом
+func (p ObjectParameters) IsInlineObject(name string) bool {
+	fld, ok := p.Fields[name]
+	if !ok {
+		return false
+	}
+
+	if fldParams, ok := fld.Params.(*ObjectParameters); ok && fldParams.Inline {
+		return true
+	}
+
+	return false
+}
+
+// GetFields возвращает поля объекта.
+// Указание withInline позволяет так же включить поля указанные во вложенных inline объектам, и получиться поля для
+// всех данных относящихся к текущему объекту.
+func (p ObjectParameters) GetFields(withInline bool) map[string]*Field {
+	fields := make(map[string]*Field)
+	p.getFields(withInline, fields)
+	return fields
+}
+
+func (p ObjectParameters) getFields(withInline bool, fields map[string]*Field) {
+	for k, f := range p.Fields {
+		if obj, ok := f.Params.(*ObjectParameters); ok && obj.Inline {
+			obj.getFields(withInline, fields)
+			continue
+		}
+		fields[k] = f
+	}
+}
+
+func (p *ObjectParameters) Merge(parameters Parameters) error {
+	op, ok := parameters.(*ObjectParameters)
+	if !ok {
+		return errors.New("invalid object parameters")
+	}
+	for k, fld := range op.Fields {
+		if f, ok := p.Fields[k]; ok {
+			if err := f.Merge(fld); err != nil {
+				return err
+			}
+		} else {
+			p.Fields[k] = fld
+		}
+	}
+	return nil
+}
+
+type ObjectType struct{}
+
+func (ObjectType) Name() string {
+	return "object"
+}
+
+func (ObjectType) NewParameters() Parameters {
+	return &ObjectParameters{}
+}
+
+func (ObjectType) IsEmpty(v interface{}) bool {
+	m := reflect.ValueOf(v)
+	return m.IsNil() || m.Len() == 0
+}
+
+type fieldNameCtx struct{}
+
+var FieldName = fieldNameCtx{}
+
+func (ObjectType) Walk(ctx context.Context, field *Field, v interface{}, fn WalkFunc, opts *WalkOptions) (interface{}, bool, error) {
+	params, ok := field.Params.(*ObjectParameters)
+	if !ok {
+		return nil, false, errors.New("field parameters required")
+	}
+
+	// Объекта нет в данных, спускаться к полям мы не будем
+	// Если необходимо что бы выполнялся Walk по полям необходимо передать пустой объект
+	// Если нужно что бы всегда объект был, это можно сделать через Default
+	if !opts.WalkSchema && v == nil {
+		return nil, false, nil
+	}
+
+	m := reflect.ValueOf(v)
+
+	if m.IsValid() {
+		if m.Kind() != reflect.Map {
+			return nil, false, errors.Errorf("incorrect type: \"%s\", expected \"map\"", m.Kind())
+		}
+	}
+
+	if !opts.WalkSchema && m.IsNil() {
+		return nil, false, nil
+	}
+
+	// Добавляем к переменным уровень объекта
+	ctx = expr.WithEnvKV(ctx, "_", v)
+
+	mapNew := make(map[string]interface{})
+
+	var merr *multierror.Error
+	var changed bool
+	for name, fld := range params.Fields {
+		ctxField := context.WithValue(ctx, FieldName, name)
+
+		// Если поле является Inline-объектом, то передаются данные текущего объекта
+		if p, ok := fld.Params.(*ObjectParameters); ok && p.Inline {
+			valueNew, valueChanged, err := fld.Walk(ctxField, v, fn, WalkOpts(opts))
+
+			if err != nil {
+				merr = multierror.Append(merr, errors.WithField(err, name))
+			}
+
+			// Значение было изменено и оно не пустое (Inline объект не активен)
+			if valueChanged && valueNew != nil {
+				changed = true
+			}
+
+			if valueNew != nil {
+				for n, v := range valueNew.(map[string]interface{}) {
+					mapNew[n] = v
+				}
+			}
+		} else {
+			// Если значение нет, мы используем nil
+			var value interface{}
+			if m.IsValid() && !m.IsZero() && !m.IsNil() {
+				fieldValue := m.MapIndex(reflect.ValueOf(name))
+				if fieldValue.IsValid() {
+					value = fieldValue.Interface()
+				}
+			}
+
+			valueNew, valueChanged, err := fld.Walk(ctxField, value, fn, WalkOpts(opts))
+
+			if err != nil {
+				merr = multierror.Append(merr, errors.WithField(err, name))
+			}
+
+			// Если значение было изменено мы заменяем его на новое
+			if valueChanged {
+				changed = true
+				value = valueNew
+			}
+
+			// Если значение не пустое, мы записываем поле в результат
+			if value != nil {
+				mapNew[name] = value
+			}
+
+		}
+	}
+
+	if merr != nil {
+		//merr.ErrorFormat = func(i []error) string {
+		//	return fmt.Sprintf("%d error(s)", len(i))
+		//}
+		return nil, false, merr
+	}
+
+	if v == nil || !m.IsValid() || m.IsZero() || m.IsNil() {
+		return nil, false, nil
+	}
+
+	// Проверяем изменилось ли количество полей объекта.
+	// Inline-объект игнорирует изменение количества полей, так как получает так же поля родительского объекта.
+	if !changed && !params.Inline {
+		changed = m.Len() != len(mapNew)
+	}
+
+	// Объект всегда возвращает новый модифицированный результат
+	return mapNew, changed, nil
+}
+
+func (ObjectType) ValidateParameters(p Parameters) error {
+	params, ok := p.(*ObjectParameters)
+	if !ok {
+		return nil
+	}
+
+	if len(params.Fields) > 0 {
+		for k := range params.Fields {
+			if !isValidName(k) {
+				return fmt.Errorf("field name '%s' must be in Latin, must not start with a number, "+
+					"must not contain spaces - only characters '_' can be used", k)
+			}
+		}
+	}
+	return nil
+}
+
+func Object(kv ...interface{}) *Field {
+	params := &ObjectParameters{Fields: make(map[string]*Field)}
+
+	if len(kv) > 0 {
+		inline, ok := kv[0].(bool)
+		if ok {
+			params.Inline = inline
+			kv = kv[1:]
+		}
+	}
+
+	var order []interface{}
+	i := 0
+	for {
+		if i+2 > len(kv) {
+			break
+		}
+		k, v := kv[i], kv[i+1]
+		name, kOk := k.(string)
+		field, vOk := v.(*Field)
+		if !kOk || !vOk {
+			break
+		}
+
+		params.Fields[name] = field
+		order = append(order, name)
+
+		err := objectType.ValidateParameters(params)
+		if err != nil {
+			panic(err.Error())
+		}
+
+		i += 2
+	}
+
+	fld := NewField(params, kv[i:]...)
+	if len(order) > 0 {
+		fld = fld.WithUI(&UI{
+			Options: map[string]interface{}{"fields": order},
+		})
+	}
+
+	return fld
+}
+
+func AddField(field *Field, name string, fld *Field) error {
+	switch params := field.Params.(type) {
+	case *ObjectParameters:
+		if params.Fields == nil {
+			params.Fields = make(map[string]*Field)
+		}
+		params.Fields[name] = fld
+	case *ArrayParameters:
+		params.Item = fld
+	default:
+		return errors.New("AddField not supported")
+	}
+
+	return nil
+}
+
+func RemoveAllFields(obj *Field) error {
+	params, ok := obj.Params.(*ObjectParameters)
+	if !ok {
+		return errors.New("obj is not an object")
+	}
+	params.Fields = make(map[string]*Field)
+	return nil
+}
diff --git a/pkg/schema/field/object_test.go b/pkg/schema/field/object_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..2ada594bcdeba09fab5dfcc95eb947e2028766d9
--- /dev/null
+++ b/pkg/schema/field/object_test.go
@@ -0,0 +1,232 @@
+package field
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestObjectField_Decode(t *testing.T) {
+	w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z")
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{
+			"Correct",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			map[string]interface{}{"name": "string", "date": "2012-11-01T22:08:41Z", "bool": true},
+			map[string]interface{}{"name": "string", "date": w, "bool": true},
+			false,
+		},
+		{
+			"Remove undefined fields",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			map[string]interface{}{"name": "string", "date": "2012-11-01T22:08:41Z", "bool": true, "extra": "string"},
+			map[string]interface{}{"name": "string", "date": w, "bool": true},
+			false,
+		},
+		{
+			"Empty data",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			map[string]interface{}{},
+			map[string]interface{}{},
+			false,
+		},
+		{
+			"Nil data",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			nil,
+			nil,
+			false,
+		},
+		{
+			"Incorrect field",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			map[string]interface{}{"name": "string", "date": "2012-11-01"},
+			"decode error: 1 error occurred:\n\t* field 'date': TimeType: decode error parsing time \"2012-11-01\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"\" as \"T\"\n\n",
+			true,
+		},
+		{
+			"Incorrect type#1",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			[]interface{}{"name", "string", "date", "2012-11-01"},
+			"decode error: incorrect type: \"slice\", expected \"map\"",
+			true,
+		},
+		{
+			"Incorrect type#2",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			"",
+			"decode error: incorrect type: \"string\", expected \"map\"",
+			true,
+		},
+		{
+			"Incorrect type#3",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			"some",
+			"decode error: incorrect type: \"string\", expected \"map\"",
+			true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Decode(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Decode() error = %v, want %v", err, tt.want.(string)))
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+				assert.Equal(t, got, tt.want, fmt.Sprintf("Decode() got = %v, want %v", got, tt.want))
+			}
+		})
+	}
+}
+
+func TestObjectField_Encode(t *testing.T) {
+	w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z")
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{
+			"Correct",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+
+			map[string]interface{}{"name": "string", "date": w, "bool": true},
+			map[string]interface{}{"bool": true, "name": "string", "date": "2012-11-01T22:08:41Z"},
+			false,
+		},
+		{
+			"Additional properties",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			map[string]interface{}{"name": "string", "date": w, "extra": "string", "bool": true},
+			map[string]interface{}{"bool": true, "name": "string", "date": "2012-11-01T22:08:41Z"},
+			false,
+		},
+		{
+			"Empty data",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			map[string]interface{}{},
+			map[string]interface{}{},
+			false,
+		},
+		{
+			"Nil data",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			nil,
+			nil,
+			false,
+		},
+		{
+			"Incorrect type#1",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			[]interface{}{},
+			"encode error: incorrect type: \"slice\", expected \"map\"",
+			true,
+		},
+		{
+			"Incorrect type#2",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			"",
+			"encode error: incorrect type: \"string\", expected \"map\"",
+			true,
+		},
+		{
+			"Incorrect type#3",
+			Object("name", String(), "date", Time(), "bool", Bool()),
+			"some",
+			"encode error: incorrect type: \"string\", expected \"map\"",
+			true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Encode(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Encode() error = %v, want %v", err, tt.want.(string)))
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+				assert.Equal(t, got, tt.want, fmt.Sprintf("Encode() got = %v, want %v", got, tt.want))
+			}
+		})
+	}
+}
+
+func TestFieldNameValidate(t *testing.T) {
+	tests := []struct {
+		testName  string
+		fieldName string
+		wantErr   bool
+	}{
+		{
+			"Correct field name",
+			"name",
+			false,
+		},
+		{
+			"Not Latin",
+			"название",
+			true,
+		},
+		{
+			"Start with a number",
+			"1name",
+			true,
+		},
+		{
+			"Contains space",
+			"field name",
+			true,
+		},
+		{
+			"Contains symbols",
+			"name!",
+			true,
+		},
+		{
+			"Contains hyphen",
+			"field-name",
+			true,
+		},
+		{
+			"Contains underscore (success)",
+			"field_name",
+			false,
+		},
+		{
+			"Start with a capital letter (success)",
+			"Name",
+			false,
+		},
+		{
+			"Contain a capital letter (success)",
+			"fieldName",
+			false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.testName, func(t *testing.T) {
+			params := &ObjectParameters{Fields: map[string]*Field{tt.fieldName: String()}}
+			err := objectType.ValidateParameters(params)
+			if tt.wantErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/field/options.go b/pkg/schema/field/options.go
new file mode 100644
index 0000000000000000000000000000000000000000..c0c6e4e14e4a0a9073dffe91031dbe3d3ceb259f
--- /dev/null
+++ b/pkg/schema/field/options.go
@@ -0,0 +1,175 @@
+package field
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+	"sync"
+)
+
+type Option interface {
+	Transform(f *Field, v interface{}) (interface{}, error)
+}
+
+type PriorityOption interface {
+	GetPriority() int
+}
+
+type NamedOption interface {
+	GetName() string
+}
+
+type OptionValidator interface {
+	ValidateOption() error
+}
+
+//type jsonTransform struct {
+//	Name    string          `json:"name"`
+//	Options json.RawMessage `json:"options,omitempty"`
+//}
+//
+//func (t *Option) MarshalJSON() ([]byte, error) {
+//	b, err := json.Marshal(t.Transformation)
+//	if err != nil {
+//		return nil, err
+//	}
+//
+//	j := jsonTransform{Name: GetOptionName(t.Transformation), Options: b}
+//
+//	return json.Marshal(&j)
+//}
+//
+//func (t *Option) UnmarshalJSON(b []byte) error {
+//	var j jsonTransform
+//	if err := json.Unmarshal(b, &j); err != nil {
+//		return err
+//	}
+//
+//	i, ok := nameToOption.Load(j.Name)
+//	if !ok {
+//		return fmt.Errorf("unknown transformer name \"%s\"", j.Name)
+//	}
+//	typ := i.(reflect.Type)
+//	val := reflect.New(typ)
+//	v := val.Interface()
+//
+//	if len(j.Options) > 0 {
+//		if err := json.Unmarshal(j.Options, v); err != nil {
+//			return err
+//		}
+//	}
+//
+//	tr, _ := v.(Transformation)
+//	*t = Option{Transformation: tr}
+//	return nil
+//}
+
+var (
+	nameToOption sync.Map
+	optionToName sync.Map
+)
+
+func GetOptionName(o interface{}) string {
+	typ := reflect.TypeOf(o)
+	if typ.Kind() == reflect.Ptr {
+		typ = typ.Elem()
+	}
+	if val, ok := optionToName.Load(typ); ok {
+		v := val.(string)
+		return v
+	}
+	return ""
+}
+
+func RegisterOption(o interface{}) {
+	var name string
+	typ := reflect.TypeOf(o)
+	if typ.Kind() == reflect.Ptr {
+		typ = typ.Elem()
+	}
+
+	if namer, ok := o.(NamedOption); ok {
+		name = namer.GetName()
+	} else {
+		name = typ.Name()
+	}
+
+	nameToOption.Store(name, typ)
+	optionToName.Store(typ, name)
+}
+
+type Options map[string]interface{}
+
+func (options *Options) Add(opts ...interface{}) {
+	if len(opts) == 0 {
+		return
+	}
+	if *options == nil {
+		*options = make(Options)
+	}
+	for _, o := range opts {
+		name := GetOptionName(o)
+		(*options)[name] = o
+	}
+}
+
+//func (options Options) MarshalJSON() ([]byte, error) {
+//	m := make(map[string]json.RawMessage)
+//
+//	for k,v := range options {
+//		name := GetOptionName(t)
+//		b, err := json.Marshal(t)
+//		if err != nil {
+//			return nil, err
+//		}
+//		m[name] = b
+//	}
+//	return json.Marshal(&m)
+//}
+
+func (options *Options) UnmarshalJSON(b []byte) error {
+	m := make(map[string]json.RawMessage)
+	*options = make(Options)
+	if err := json.Unmarshal(b, &m); err != nil {
+		return err
+	}
+
+	for name, opts := range m {
+		i, ok := nameToOption.Load(name)
+		if !ok {
+			return fmt.Errorf("unknown option name \"%s\"", name)
+		}
+		typ := i.(reflect.Type)
+		val := reflect.New(typ)
+		v := val.Interface()
+		if len(opts) > 0 {
+			if err := json.Unmarshal(opts, v); err != nil {
+				return err
+			}
+		}
+		if validator, ok := v.(OptionValidator); ok {
+			err := validator.ValidateOption()
+			if errors.Is(err, ErrSkipOption) {
+				continue
+			}
+			if err != nil {
+				return err
+			}
+		}
+		options.Add(v)
+	}
+	return nil
+}
+
+func (options Options) Transform(field *Field, v interface{}) (interface{}, error) {
+	var err error
+	for _, t := range options {
+		o := t.(Option)
+		v, err = o.Transform(field, v)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return v, nil
+}
diff --git a/pkg/schema/field/primary_key.go b/pkg/schema/field/primary_key.go
new file mode 100644
index 0000000000000000000000000000000000000000..b0b26e16a91cd506231ba46307485290a138bfb8
--- /dev/null
+++ b/pkg/schema/field/primary_key.go
@@ -0,0 +1,55 @@
+package field
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+)
+
+//TODO readonly
+
+var primaryKeyType = &PrimaryKeyType{}
+
+type PrimaryKeyParameters struct{}
+
+func (p PrimaryKeyParameters) Type() Type                   { return primaryKeyType }
+func (p *PrimaryKeyParameters) Clone(reset bool) Parameters { return p }
+
+type PrimaryKeyType struct{}
+
+func (p PrimaryKeyType) Name() string {
+	return "PrimaryKey"
+}
+
+func (PrimaryKeyType) NewParameters() Parameters {
+	return &PrimaryKeyParameters{}
+}
+
+func (PrimaryKeyType) IsEmpty(v interface{}) bool {
+	s, _ := v.(string)
+	return s == ""
+}
+
+func (PrimaryKeyType) Decode(_ context.Context, _ *Field, v interface{}) (interface{}, error) {
+	if v == nil {
+		return nil, nil
+	}
+	if _, ok := v.(string); ok {
+		return v, nil
+	}
+	return nil, fmt.Errorf("PrimaryKeyField decode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind())
+}
+
+func (PrimaryKeyType) Encode(_ context.Context, _ *Field, v interface{}) (interface{}, error) {
+	if v == nil {
+		return nil, nil
+	}
+	if _, ok := v.(string); ok {
+		return v, nil
+	}
+	return nil, fmt.Errorf("PrimaryKeyField encode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind())
+}
+
+func PrimaryKey(o ...interface{}) *Field {
+	return NewField(&PrimaryKeyParameters{}, o...)
+}
diff --git a/pkg/schema/field/primary_key_test.go b/pkg/schema/field/primary_key_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f74f32cafc72612d9b1575561718e6c625334d53
--- /dev/null
+++ b/pkg/schema/field/primary_key_test.go
@@ -0,0 +1,56 @@
+package field
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestPrimaryKeyField_Decode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", PrimaryKey(), "custom_id", "custom_id", false},
+		{"Wrong data", PrimaryKey(), 2, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Decode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Decode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestPrimaryKeyField_Encode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", PrimaryKey(), "primary key", "primary key", false},
+		{"Wrong data", PrimaryKey(), 2, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Encode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Decode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/field/string.go b/pkg/schema/field/string.go
new file mode 100644
index 0000000000000000000000000000000000000000..b7e548b65f5c1572cc515cad19192899843ab62f
--- /dev/null
+++ b/pkg/schema/field/string.go
@@ -0,0 +1,53 @@
+package field
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+)
+
+var stringType = &StringType{}
+
+type StringParameters struct{}
+
+func (s StringParameters) Type() Type                   { return stringType }
+func (s *StringParameters) Clone(reset bool) Parameters { return s }
+
+type StringType struct{}
+
+func (s StringType) Name() string {
+	return "string"
+}
+
+func (StringType) NewParameters() Parameters {
+	return &StringParameters{}
+}
+
+func (StringType) IsEmpty(v interface{}) bool {
+	s, _ := v.(string)
+	return s == ""
+}
+
+func (StringType) Decode(_ context.Context, _ *Field, v interface{}) (interface{}, error) {
+	if v == nil {
+		return nil, nil
+	}
+	if _, ok := v.(string); ok {
+		return v, nil
+	}
+	return nil, fmt.Errorf("StringField decode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind())
+}
+
+func (StringType) Encode(_ context.Context, _ *Field, v interface{}) (interface{}, error) {
+	if v == nil {
+		return nil, nil
+	}
+	if _, ok := v.(string); ok {
+		return v, nil
+	}
+	return nil, fmt.Errorf("StringField encode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind())
+}
+
+func String(o ...interface{}) *Field {
+	return NewField(&StringParameters{}, o...)
+}
diff --git a/pkg/schema/field/string_test.go b/pkg/schema/field/string_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..d0fca29b8ed3f0c4ec987bf6705b7ea435116e40
--- /dev/null
+++ b/pkg/schema/field/string_test.go
@@ -0,0 +1,56 @@
+package field
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestStringField_Decode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", String(), "string", "string", false},
+		{"Wrong data", String(), 2, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Decode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Decode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestStringField_Encode(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", String(), "string", "string", false},
+		{"Wrong data", String(), 2, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Encode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Decode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/field/time.go b/pkg/schema/field/time.go
new file mode 100644
index 0000000000000000000000000000000000000000..064906f236371d2914a0544c3f6abef83fd77f65
--- /dev/null
+++ b/pkg/schema/field/time.go
@@ -0,0 +1,85 @@
+package field
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+const DefaultTimeLayout = time.RFC3339
+
+var timeType = &TimeType{}
+
+type TimeParameters struct {
+	Layout string `json:"layout,omitempty"`
+}
+
+func (p TimeParameters) Type() Type                  { return timeType }
+func (p TimeParameters) Clone(reset bool) Parameters { return &p }
+
+func (p TimeParameters) GetLayout() string {
+	if p.Layout != "" {
+		return p.Layout
+	}
+	return DefaultTimeLayout
+}
+
+type TimeType struct{}
+
+func (TimeType) Name() string {
+	return "time"
+}
+
+func (TimeType) NewParameters() Parameters {
+	return &TimeParameters{}
+}
+
+func (TimeType) IsEmpty(v interface{}) bool {
+	t, _ := v.(time.Time)
+	return t.IsZero()
+}
+
+func (TimeType) Decode(_ context.Context, field *Field, v interface{}) (interface{}, error) {
+	params, ok := field.Params.(*TimeParameters)
+	if !ok {
+		return nil, errors.New("TimeType: field type parameters required")
+	}
+
+	if v == nil {
+		return v, nil
+	}
+	switch val := v.(type) {
+
+	case string:
+		if t, err := time.Parse(params.GetLayout(), val); err != nil {
+			return nil, fmt.Errorf("TimeType: decode error %w", err)
+		} else {
+			return t, nil
+		}
+	case time.Time:
+		return v, nil
+	}
+	return nil, fmt.Errorf("TimeType: decode: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind())
+}
+
+func (TimeType) Encode(_ context.Context, field *Field, v interface{}) (interface{}, error) {
+	params, ok := field.Params.(*TimeParameters)
+	if !ok {
+		return nil, errors.New("TimeType: field type parameters required")
+	}
+
+	if v == nil {
+		return v, nil
+	}
+	if t, ok := v.(time.Time); ok {
+		return t.Format(params.GetLayout()), nil
+	}
+	return nil, fmt.Errorf("TimeType: encode: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind())
+}
+
+func Time(o ...interface{}) *Field {
+	return NewField(&TimeParameters{}, o...)
+}
diff --git a/pkg/schema/field/time_test.go b/pkg/schema/field/time_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f9a8fb44552a0e0a704f7baae7ab10019c889bec
--- /dev/null
+++ b/pkg/schema/field/time_test.go
@@ -0,0 +1,61 @@
+package field
+
+import (
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestTimeField_Decode(t *testing.T) {
+	w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z")
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", Time(), "2012-11-01T22:08:41Z", w, false},
+		{"Incorrect format", Time(), "2012-11-01", nil, true},
+		{"Incorrect type", Time(), 2, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Decode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Decode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestTimeField_Encode(t *testing.T) {
+	w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z")
+	tests := []struct {
+		name    string
+		field   *Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Correct", Time(), w, "2012-11-01T22:08:41Z", false},
+		{"Incorrect type string", Time(), "2012-11-01T22:08:41Z", nil, true},
+		{"Incorrect type int", Time(), 2, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := Encode(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Encode() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Encode() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/field/type.go b/pkg/schema/field/type.go
new file mode 100644
index 0000000000000000000000000000000000000000..edae76734d562d2546d3b68e89e125c7a87744a6
--- /dev/null
+++ b/pkg/schema/field/type.go
@@ -0,0 +1,47 @@
+package field
+
+import (
+	"fmt"
+	"reflect"
+	"sync"
+)
+
+var (
+	registry sync.Map
+)
+
+// Parameters - интерфейс который должен реализовывать параметр конкретного типа
+type Parameters interface {
+	Type() Type
+	Clone(reset bool) Parameters
+}
+
+// Type - тип поля, отвечает за получение, кодирование и декодирование параметров для данного типа
+type Type interface {
+	Name() string
+	NewParameters() Parameters
+	//Encode(ctx *Context.Context, field *Field, v interface{}) (interface{}, error)
+	//Decode(ctx *Context.Context, field *Field, v interface{}) (interface{}, error)
+}
+
+func Register(typ Type) {
+	registry.Store(typ.Name(), typ)
+}
+
+func Unregister(typ interface{}) {
+	switch t := typ.(type) {
+	case Type:
+		registry.Delete(t.Name())
+	case string:
+		registry.Delete(t)
+	default:
+		panic(fmt.Sprintf("unknown type: \"%s\"", reflect.ValueOf(t).Kind()))
+	}
+}
+
+func GetType(name string) (Type, bool) {
+	if i, ok := registry.Load(name); ok {
+		return i.(Type), true
+	}
+	return nil, false
+}
diff --git a/pkg/schema/field/unknown.go b/pkg/schema/field/unknown.go
new file mode 100644
index 0000000000000000000000000000000000000000..f28a51cd1fbdc20a6b026d4504872149c939e727
--- /dev/null
+++ b/pkg/schema/field/unknown.go
@@ -0,0 +1,57 @@
+package field
+
+import (
+	"encoding/json"
+
+	jsoniter "github.com/json-iterator/go"
+	"github.com/pkg/errors"
+)
+
+var unknownType = &UnknownType{}
+
+type UnknownParameters struct {
+	Typ    string          `json:"type,omitempty"`
+	Params json.RawMessage `json:"params,omitempty"`
+}
+
+func (UnknownParameters) Type() Type                    { return unknownType }
+func (p UnknownParameters) Clone(reset bool) Parameters { return &p }
+
+type UnknownType struct{}
+
+func (UnknownType) Name() string {
+	return "unknown"
+}
+
+func (UnknownType) NewParameters() Parameters {
+	return &UnknownParameters{}
+}
+
+func (UnknownType) ConvertParameters(p Parameters) (Parameters, error) {
+	unknownParams, ok := p.(*UnknownParameters)
+	if !ok {
+		return p, nil
+	}
+
+	// Проверяем возможность восстановления исходного типа поля и возвращаем его если возможно
+	typ, ok := GetType(unknownParams.Typ)
+	if !ok {
+		return p, nil
+	}
+
+	params := typ.NewParameters()
+	if len(unknownParams.Params) > 0 {
+		if err := jsoniter.Unmarshal(unknownParams.Params, params); err != nil {
+			return p, errors.Wrapf(err, "error recover field type %s", typ.Name())
+		}
+	}
+
+	return params, nil
+}
+
+func Unknown(typ string, params json.RawMessage, o ...interface{}) *Field {
+	var pc ParametersConverter
+	pc = unknownType
+	_ = pc
+	return NewField(&UnknownParameters{Typ: typ, Params: params}, o...)
+}
diff --git a/pkg/schema/field/walk.go b/pkg/schema/field/walk.go
new file mode 100644
index 0000000000000000000000000000000000000000..d7b24a10098944b6c742421027075a0e2febe54e
--- /dev/null
+++ b/pkg/schema/field/walk.go
@@ -0,0 +1,57 @@
+package field
+
+import "context"
+
+type WalkFuncResult struct {
+	Context context.Context
+	Value   interface{}
+	Changed bool
+	Stop    bool
+}
+
+// WalkFunc - функция которая будет применена к значению каждого поля в результате обход данных функцией `Walk`
+// Возвращает новое значение если таковое имеется, признак изменения данных и ошибку в случае возникновения
+type WalkFunc func(ctx context.Context, fld *Field, v interface{}) (result WalkFuncResult, err error)
+
+// FieldWalker - интерфейс позволяющий выполнить обход предоставленного поля
+// Реализуется в типах
+type FieldWalker interface {
+	// Walk выполняет обход данных в соответствии с полем. Функция возвращает результат:
+	// результат обхода, флаг что данные изменены, ошибку
+	Walk(ctx context.Context, fld *Field, v interface{}, fn WalkFunc, opts *WalkOptions) (interface{}, bool, error)
+}
+
+type WalkOptions struct {
+	WalkSchema bool
+}
+
+type WalkOption func(opts *WalkOptions)
+
+// WalkSchema указывает что необходимо выполнять обход по схеме, вместо обхода по данным.
+// При обходе по данным в случае отсутствия данных для поля дальнейшая обработка данного поля не выполняется.
+// При обходе по схемы будут обработаны все поля присутствующие в схеме вне зависимости от наличия данных.
+func WalkSchema() WalkOption {
+	return func(opts *WalkOptions) {
+		opts.WalkSchema = true
+	}
+}
+
+func WalkOpts(o *WalkOptions) WalkOption {
+	return func(opts *WalkOptions) {
+		*opts = *o
+	}
+}
+
+func NewWalkOptions(opt ...WalkOption) *WalkOptions {
+	opts := &WalkOptions{}
+	for _, o := range opt {
+		o(opts)
+	}
+	return opts
+}
+
+// Walker - интерфейс позволяющий выполнить обход данных и содержит схему внутри
+// Реализовано в Field, Schema
+type Walker interface {
+	Walk(ctx context.Context, v interface{}, fn WalkFunc, opt ...WalkOption) (interface{}, bool, error)
+}
diff --git a/pkg/schema/loader.go b/pkg/schema/loader.go
new file mode 100644
index 0000000000000000000000000000000000000000..5a646e2398eec49bdd1b8be135f9b9a54952a113
--- /dev/null
+++ b/pkg/schema/loader.go
@@ -0,0 +1,25 @@
+package schema
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+var defaultLoader field.Loader
+
+func SetDefaultLoader(l field.Loader) {
+	defaultLoader = l
+}
+
+func GetLoader() field.Loader {
+	return defaultLoader
+}
+
+func Load(ctx context.Context, ref string) (*Schema, error) {
+	s := New()
+	if err := s.Field.LoadRef(ctx, ref, GetLoader()); err != nil {
+		return nil, err
+	}
+	return s, nil
+}
diff --git a/pkg/schema/modify/default.go b/pkg/schema/modify/default.go
new file mode 100644
index 0000000000000000000000000000000000000000..66ce6b89c0619ceb232050fa5ab057e4330fdfcb
--- /dev/null
+++ b/pkg/schema/modify/default.go
@@ -0,0 +1,47 @@
+package modify
+
+import (
+	"context"
+	"encoding/json"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+type defaultValue struct {
+	Value interface{}
+	ready bool
+}
+
+func (d *defaultValue) Prepare(f *field.Field) error {
+	var err error
+	d.Value, err = field.Decode(nil, f, d.Value)
+	return err
+}
+
+func (d *defaultValue) UnmarshalJSON(bytes []byte) error {
+	return json.Unmarshal(bytes, &d.Value)
+}
+
+func (d defaultValue) MarshalJSON() ([]byte, error) {
+	return json.Marshal(d.Value)
+}
+
+func Default(v interface{}) Modifier {
+	return &defaultValue{Value: v}
+}
+
+func (defaultValue) GetName() string { return "default" }
+
+func (d defaultValue) Modify(ctx context.Context, f *field.Field, v interface{}) (interface{}, bool, error) {
+	if !d.ready {
+		var err error
+		if d.Value, err = field.Decode(ctx, f, d.Value); err != nil {
+			return nil, false, err
+		}
+	}
+
+	if v == nil && d.Value != nil {
+		return d.Value, true, nil
+	}
+	return v, false, nil
+}
diff --git a/pkg/schema/modify/default_test.go b/pkg/schema/modify/default_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b99630dc65a9249d04bc1b29ac9e9fc902c745cf
--- /dev/null
+++ b/pkg/schema/modify/default_test.go
@@ -0,0 +1,58 @@
+package modify
+
+import (
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDefault(t *testing.T) {
+	now := time.Now()
+	_ = now
+	w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z")
+
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"String value", field.String(Default("some")), "same", "same", false},
+		{"String", field.String(Default("some")), nil, "some", false},
+		{"Int", field.Number(field.NumberFormatInt, Default(2)), nil, int64(2), false},
+		{"Time", field.Time(Default(now)), nil, now, false},
+		{"Bool", field.Bool(Default(true)), nil, true, false},
+		{"Object: nil with field default", field.Object("name", field.String(Default("test"))), nil, nil, false},
+		{"Object: nil with object default", field.Object("name", field.String(Default("test"))).AddOptions(Default(map[string]interface{}{"name": "a"})), nil, map[string]interface{}{"name": "a"}, false},
+		{"Object: empty", field.Object(
+			"a", field.String(Default("a")),
+			"b", field.Number(field.NumberFormatInt, Default(1)),
+			"c", field.String(),
+		),
+			map[string]interface{}{},
+			map[string]interface{}{"a": "a", "b": int64(1)},
+			false},
+		{"Array of time",
+			field.Object("array", field.Array(field.Time())),
+			map[string]interface{}{"array": []interface{}{"2012-11-01T22:08:41Z", "2012-11-01T22:08:41Z"}},
+			map[string]interface{}{"array": []interface{}{w, w}},
+			false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := field.Decode(nil, tt.field, tt.data)
+			require.NoError(t, err)
+			got, _, err = Modify(nil, tt.field, got)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Modify() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/pkg/schema/modify/modify.go b/pkg/schema/modify/modify.go
new file mode 100644
index 0000000000000000000000000000000000000000..191a428c4efac916511dd37481d7e1bac00a87bc
--- /dev/null
+++ b/pkg/schema/modify/modify.go
@@ -0,0 +1,106 @@
+package modify
+
+import (
+	"context"
+	"sort"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+const ModifierPriority = 1000
+
+type Modifier interface {
+	Modify(ctx context.Context, f *field.Field, v interface{}) (interface{}, bool, error)
+}
+
+type Modifiers []Modifier
+
+func (l Modifiers) Len() int { return len(l) }
+func (l Modifiers) Less(i, j int) bool {
+	pi, pj := ModifierPriority, ModifierPriority
+	if o, ok := l[i].(field.PriorityOption); ok {
+		pi = o.GetPriority()
+	}
+	if o, ok := l[j].(field.PriorityOption); ok {
+		pj = o.GetPriority()
+	}
+	if pi == pj {
+		return field.GetOptionName(l[i]) < field.GetOptionName(l[j])
+	}
+	return pi < pj
+}
+func (l Modifiers) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
+
+func getModifiers(f *field.Field) Modifiers {
+	var ms Modifiers
+	for _, o := range f.Options {
+		if m, ok := o.(Modifier); ok {
+			ms = append(ms, m)
+		}
+	}
+	sort.Sort(ms)
+	return ms
+}
+
+func applyModifiers(ctx context.Context, f *field.Field, v interface{}) (interface{}, bool, error) {
+	var err error
+	var ok, modified bool
+	modifiers := getModifiers(f)
+	for _, i := range modifiers {
+		v, ok, err = i.Modify(ctx, f, v)
+		if err != nil {
+			return nil, false, err
+		}
+		modified = modified || ok
+	}
+	return v, modified, nil
+}
+
+func Modify(ctx context.Context, w field.Walker, v interface{}) (interface{}, bool, error) {
+	if m, ok := v.(map[string]interface{}); ok {
+		ctx = expr.WithEnv(ctx, m)
+	}
+
+	v, c, err := w.Walk(ctx, v, func(ctx context.Context, fld *field.Field, v interface{}) (res field.WalkFuncResult, err error) {
+		var vv interface{}
+		var changed bool
+
+		if vv, changed, err = applyModifiers(ctx, fld, v); err != nil {
+			return
+		}
+
+		if changed {
+			v = vv
+		}
+
+		if modifier, ok := fld.GetType().(Modifier); ok {
+			vv, ch, err := modifier.Modify(ctx, fld, v)
+
+			if err != nil {
+				return res, err
+			}
+
+			if ch {
+				v = vv
+			}
+		}
+
+		res.Value = v
+		res.Changed = changed
+		return
+	})
+
+	if err != nil {
+		return nil, false, errors.Wrap(err, "modification error")
+	}
+
+	return v, c, nil
+}
+
+func init() {
+	field.RegisterOption(trimSpace(true))
+	field.RegisterOption(defaultValue{})
+	field.RegisterOption(value{})
+}
diff --git a/pkg/schema/modify/string.go b/pkg/schema/modify/string.go
new file mode 100644
index 0000000000000000000000000000000000000000..2f0ecc6f24e114f89d97b5e0d54251da68c4aebf
--- /dev/null
+++ b/pkg/schema/modify/string.go
@@ -0,0 +1,33 @@
+package modify
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+type trimSpace bool
+
+func TrimSpace() Modifier {
+	t := trimSpace(true)
+	return &t
+}
+
+func (c trimSpace) Modify(ctx context.Context, field *field.Field, v interface{}) (interface{}, bool, error) {
+	if !c {
+		return v, false, nil
+	}
+
+	if v == nil {
+		return nil, false, nil
+	}
+
+	if s, ok := v.(string); ok {
+		s = strings.TrimSpace(s)
+		return s, true, nil
+	}
+	return nil, false, fmt.Errorf("incorrect type: \"%s\", expected \"string\"", reflect.ValueOf(v).Kind())
+}
diff --git a/pkg/schema/modify/string_test.go b/pkg/schema/modify/string_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ef90f7f6f4d6a43ff13b3ac10bda68602f09c5f4
--- /dev/null
+++ b/pkg/schema/modify/string_test.go
@@ -0,0 +1,78 @@
+package modify
+
+import (
+	"reflect"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestTrimSpace(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"String with spaces", field.String().AddOptions(TrimSpace()), "    string string   ", "string string", false},
+		{"Nil", field.String().AddOptions(TrimSpace()), nil, nil, false},
+		{"Spaces", field.String().AddOptions(TrimSpace()), "      ", "", false},
+		{"Empty string", field.String().AddOptions(TrimSpace()), "", "", false},
+		{"Not a string", field.String().AddOptions(TrimSpace()), 2, nil, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, _, err := Modify(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Modify() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Modify() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestModify(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+		error   string
+	}{
+		{"String Length Max", field.String().AddOptions(TrimSpace()), " ", false, ""},
+		{"String Length Min", field.String().AddOptions(TrimSpace()), " ", false, ""},
+		{"Nil Length Max", field.String().AddOptions(TrimSpace()), nil, false, ""},
+		{"Nil Length Min", field.String().AddOptions(TrimSpace()), nil, false, ""},
+		{"Int Length Max", field.String().AddOptions(TrimSpace()), 1, true, "modification error: incorrect type: \"int\", expected \"string\""},
+		{"Int Length Min", field.String().AddOptions(TrimSpace()), 1, true, "modification error: incorrect type: \"int\", expected \"string\""},
+		{"Float Length Max", field.String().AddOptions(TrimSpace()), 1.0, true, "modification error: incorrect type: \"float64\", expected \"string\""},
+		{"Float Length Min", field.String().AddOptions(TrimSpace()), 1.0, true, "modification error: incorrect type: \"float64\", expected \"string\""},
+		{"Bool Length Max", field.String().AddOptions(TrimSpace()), true, true, "modification error: incorrect type: \"bool\", expected \"string\""},
+		{"Bool Length Min", field.String().AddOptions(TrimSpace()), true, true, "modification error: incorrect type: \"bool\", expected \"string\""},
+		{"Array Length Max", field.String().AddOptions(TrimSpace()), [1]string{""}, true, "modification error: incorrect type: \"array\", expected \"string\""},
+		{"Array Length Min", field.String().AddOptions(TrimSpace()), [1]string{""}, true, "modification error: incorrect type: \"array\", expected \"string\""},
+		{"Slice Length Max", field.String().AddOptions(TrimSpace()), []string{""}, true, "modification error: incorrect type: \"slice\", expected \"string\""},
+		{"Slice Length Min", field.String().AddOptions(TrimSpace()), []string{""}, true, "modification error: incorrect type: \"slice\", expected \"string\""},
+		{"Map Length Max", field.String().AddOptions(TrimSpace()), map[string]string{"": ""}, true, "modification error: incorrect type: \"map\", expected \"string\""},
+		{"Map Length Min", field.String().AddOptions(TrimSpace()), map[string]string{"": ""}, true, "modification error: incorrect type: \"map\", expected \"string\""},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, _, err := Modify(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.error)
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/modify/value.go b/pkg/schema/modify/value.go
new file mode 100644
index 0000000000000000000000000000000000000000..1f87c80387c802eaf7fb7e3fec221ea263092050
--- /dev/null
+++ b/pkg/schema/modify/value.go
@@ -0,0 +1,39 @@
+package modify
+
+import (
+	"context"
+	"encoding/json"
+	"reflect"
+
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/pkg/errors"
+)
+
+type value struct {
+	Expression string
+}
+
+func (d *value) UnmarshalJSON(bytes []byte) error {
+	return json.Unmarshal(bytes, &d.Expression)
+}
+
+func (d value) MarshalJSON() ([]byte, error) {
+	return json.Marshal(d.Expression)
+}
+
+func Value(exp string) Modifier {
+	return &value{Expression: exp}
+}
+
+func (d value) Modify(ctx context.Context, f *field.Field, v interface{}) (interface{}, bool, error) {
+	val, err := expr.EvalKV(ctx, d.Expression, "_value", v)
+	if err != nil {
+		return nil, false, errors.Wrap(err, "value evaluation error")
+	}
+	// использование reflect.DeepEqual для учета случаев, когда в val массив или объект
+	if !reflect.DeepEqual(val, v) {
+		return val, true, nil
+	}
+	return v, false, nil
+}
diff --git a/pkg/schema/modify/value_test.go b/pkg/schema/modify/value_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..9878fc70663005f7650ac567899634a94446e8b9
--- /dev/null
+++ b/pkg/schema/modify/value_test.go
@@ -0,0 +1,63 @@
+package modify
+
+import (
+	"reflect"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+func TestValue(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		want    interface{}
+		wantErr bool
+	}{
+		{"Condition with _value", field.String().AddOptions(Value("_value == 'ab' ? _value : _value + 'b'")), "a", "ab", false},
+		{"Condition with _value", field.String().AddOptions(Value("_value == 'ab' ? _value : _value + 'b'")), "ab", "ab", false},
+		{"Default with _value", field.String().AddOptions(Value("_value == nil ? 'abc' : _value")), nil, "abc", false},
+		{"Field global", field.Object(
+			"fld1", field.String(),
+			"fld2", field.String().AddOptions(Value("fld1 + 'b'")),
+		),
+			map[string]interface{}{"fld1": "a"},
+			map[string]interface{}{"fld1": "a", "fld2": "ab"}, false},
+		{"Field local", field.Object(
+			"fld1", field.String(),
+			"fld2", field.String().AddOptions(Value("_.fld1 + 'b'")),
+		),
+			map[string]interface{}{"fld1": "a"},
+			map[string]interface{}{"fld1": "a", "fld2": "ab"}, false},
+		{"Multiple fields ", field.Object(
+			"fld1", field.String(),
+			"fld2", field.String(),
+			"fld3", field.String(),
+			"fld4", field.String(),
+			"fld5", field.String().AddOptions(Value("_.fld1 + fld2 + _.fld3+fld4")),
+		),
+			map[string]interface{}{"fld1": "a", "fld2": "b", "fld3": "c", "fld4": "d"},
+			map[string]interface{}{"fld1": "a", "fld2": "b", "fld3": "c", "fld4": "d", "fld5": "abcd"},
+			false},
+		{"Nil field error", field.Object(
+			"fld1", field.String(),
+			"fld2", field.String().AddOptions(Value("_.fld1 + 'b'")),
+		),
+			map[string]interface{}{},
+			map[string]interface{}{},
+			true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, _, err := Modify(nil, tt.field, tt.data)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Modify() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Modify() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go
new file mode 100644
index 0000000000000000000000000000000000000000..e0eca6b6ffc02511acbe3712b2f2bf83fc5bc05e
--- /dev/null
+++ b/pkg/schema/schema.go
@@ -0,0 +1,215 @@
+package schema
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/modify"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/validate"
+)
+
+type Schema struct {
+	field.Field
+	Loaded bool `json:"loaded"`
+}
+
+func New(kv ...interface{}) *Schema {
+	return &Schema{Field: *field.Object(kv...)}
+}
+
+func NewFromField(f *field.Field) *Schema {
+	return &Schema{Field: *f}
+}
+
+var (
+	Encode   = field.Encode
+	Decode   = field.Decode
+	Modify   = modify.Modify
+	Validate = validate.Validate
+	Evaluate = field.Evaluate
+)
+
+func (s *Schema) Clone(reset bool) *Schema {
+	return &Schema{
+		Field:  *s.Field.Clone(reset),
+		Loaded: s.Loaded,
+	}
+}
+
+func (s Schema) WithIncludes(includes ...interface{}) *Schema {
+	s.Field.SetIncludes(includes...)
+	return &s
+}
+
+func (s *Schema) Load(ctx context.Context) error {
+	if s.Loaded {
+		return nil
+	}
+	return s.LoadIncludes(ctx, nil)
+}
+
+func (s *Schema) LoadIncludes(ctx context.Context, loader field.Loader) (err error) {
+	if loader == nil {
+		loader = GetLoader()
+	}
+	err = s.Field.LoadIncludes(ctx, loader)
+	if err == nil {
+		s.Loaded = true
+	}
+	return
+}
+
+func (s *Schema) Modify(ctx context.Context, data map[string]interface{}) (res map[string]interface{}, err error) {
+	if err = s.Load(ctx); err != nil {
+		return nil, err
+	}
+
+	v, _, err := Modify(ctx, s, data)
+	if err != nil || v == nil {
+		return
+	}
+
+	res, _ = v.(map[string]interface{})
+	return
+}
+
+func (s *Schema) Validate(ctx context.Context, data map[string]interface{}) (err error) {
+	if err = s.Load(ctx); err != nil {
+		return err
+	}
+
+	return Validate(ctx, s, data)
+}
+
+func (s *Schema) Evaluate(ctx context.Context, data map[string]interface{}) (res map[string]interface{}, err error) {
+	if err = s.Load(ctx); err != nil {
+		return nil, err
+	}
+
+	v, err := Evaluate(ctx, s, data)
+	if err != nil || v == nil {
+		return
+	}
+	res, _ = v.(map[string]interface{})
+	return
+}
+
+func (s *Schema) Decode(ctx context.Context, v interface{}) (res map[string]interface{}, err error) {
+	if err = s.Load(ctx); err != nil {
+		return nil, err
+	}
+
+	if v, err = Decode(ctx, s, v); err != nil {
+		return nil, err
+	}
+	res, _ = v.(map[string]interface{})
+	return
+}
+
+func (s *Schema) Encode(ctx context.Context, v interface{}) (interface{}, error) {
+	if err := s.Load(ctx); err != nil {
+		return nil, err
+	}
+
+	var res interface{}
+	var err error
+
+	if res, err = Encode(ctx, s, v); err != nil {
+		return nil, err
+	}
+
+	return res, nil
+}
+
+func (s *Schema) ToValue(ctx context.Context, data map[string]interface{}) (res map[string]interface{}, err error) {
+	if err = s.Load(ctx); err != nil {
+		return nil, err
+	}
+
+	if data, err = s.Decode(ctx, data); err != nil {
+		return nil, err
+	}
+	if data, err = s.Modify(ctx, data); err != nil {
+		return nil, err
+	}
+	if data, err = s.Evaluate(ctx, data); err != nil {
+		return nil, err
+	}
+	if err = s.Validate(ctx, data); err != nil {
+		return nil, err
+	}
+	return data, err
+}
+
+type parentFieldCtxKey struct{}
+
+func (s *Schema) Introspect(ctx context.Context, data map[string]interface{}) (map[string]interface{}, *Schema, error) {
+	if err := s.Load(ctx); err != nil {
+		return nil, nil, err
+	}
+
+	var err error
+
+	chg := true
+	val := data
+	i := 0
+
+	var mutatedSchema *Schema
+
+	for chg {
+		mutatedSchema = nil
+
+		var res interface{}
+		res, chg, err = s.Walk(expr.WithEnv(ctx, val), val, func(ctx context.Context, f *field.Field, v interface{}) (res field.WalkFuncResult, err error) {
+			parent, _ := ctx.Value(parentFieldCtxKey{}).(*field.Field)
+			name, _ := ctx.Value(field.FieldName).(string)
+			enabled, err := f.IsEnabled(ctx)
+			if err != nil {
+				return
+			}
+
+			if !enabled {
+				res.Stop = true
+				if v != nil {
+					res.Changed = true
+				}
+				return
+			}
+
+			fld := f.Clone(true)
+			if mutatedSchema == nil {
+				mutatedSchema = &Schema{Field: *fld}
+				fld = &mutatedSchema.Field
+			}
+
+			if parent != nil && name != "" {
+				field.AddField(parent, name, fld)
+			}
+
+			ctx = context.WithValue(ctx, parentFieldCtxKey{}, fld)
+			res.Context = ctx
+
+			return
+		}, field.WalkSchema())
+
+		if err != nil {
+			return nil, nil, errors.Wrap(err, "evaluation error")
+		}
+
+		if res != nil {
+			val = res.(map[string]interface{})
+		} else {
+			val = nil
+		}
+
+		i += 1
+
+		if i > field.EvaluatePassesLimit {
+			return nil, nil, errors.New("fail to evaluate data conditions")
+		}
+	}
+
+	return val, mutatedSchema, nil
+}
diff --git a/pkg/schema/schema_json.go b/pkg/schema/schema_json.go
new file mode 100644
index 0000000000000000000000000000000000000000..906acb5d0d361611d01f43d10bd0f80377ccb4a7
--- /dev/null
+++ b/pkg/schema/schema_json.go
@@ -0,0 +1,54 @@
+package schema
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	jsoniter "github.com/json-iterator/go"
+)
+
+type jsonSchema struct {
+	//Field  json.RawMessage `json:"field,inline"`
+	Loaded bool `json:"loaded"`
+}
+
+func (s *Schema) UnmarshalJSON(b []byte) error {
+
+	var j *jsonSchema
+	if err := jsoniter.Unmarshal(b, &j); err != nil {
+		return errors.Wrapf(err, "error unmarshal json into field")
+	}
+	s.Loaded = j.Loaded
+
+	if err := s.Field.UnmarshalJSON(b); err != nil {
+		return err
+	}
+
+	//if len(j.Field) > 0 {
+	//	if err := s.Field.UnmarshalJSON(j.Field); err != nil {
+	//		return err
+	//	}
+	//	//if err := jsoniter.Unmarshal(j.Field, &s.Field); err != nil {
+	//	//	return err
+	//	//}
+	//}
+
+	return nil
+}
+
+func (s *Schema) MarshalJSON() ([]byte, error) {
+
+	jsonField, err := s.Field.MarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+
+	jsonSch, err := jsoniter.Marshal(jsonSchema{
+		//Field: b,
+		Loaded: s.Loaded,
+	})
+	if err != nil {
+		return nil, err
+	}
+	jsonSch[0] = ',' // вместо такого можно сначала jsonField размаршалить в map[string]interface{}, добавить поле и замаршалить еще раз
+
+	return append(jsonField[:len(jsonField)-1], jsonSch...), nil
+}
diff --git a/pkg/schema/test/object_test.go b/pkg/schema/test/object_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e5af975cdb9ed8e86044e5d357530dd2420afee9
--- /dev/null
+++ b/pkg/schema/test/object_test.go
@@ -0,0 +1,1490 @@
+package test
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/data"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/modify"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/validate"
+	"github.com/hashicorp/go-multierror"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDefaultTimeField_JSON(t *testing.T) {
+	w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z")
+	fld := field.Object(
+		"timeField", field.Time(modify.Default(w)),
+	)
+
+	b, err := json.MarshalIndent(fld, "", "  ")
+	require.NoError(t, err)
+
+	res := &field.Field{}
+	err = json.Unmarshal(b, res)
+	require.NoError(t, err)
+
+	assert.Equal(t, fld, res)
+}
+
+func TestStringField_JSON(t *testing.T) {
+	fld := field.String().AddOptions(validate.MaxLength(200), modify.TrimSpace())
+
+	b, err := json.MarshalIndent(fld, "", "  ")
+	require.NoError(t, err)
+
+	res := field.NewField(nil)
+	err = json.Unmarshal(b, res)
+	require.NoError(t, err)
+
+	assert.Equal(t, fld, res)
+}
+
+func TestNumberField_JSON(t *testing.T) {
+	fld := field.Number(field.NumberFormatInt).AddOptions(
+		validate.Min(0),
+		validate.Max(10),
+		validate.MultipleOf(2),
+		validate.Enum(
+			validate.EnumOpt{
+				Name:  "N 1",
+				Value: 1.0,
+			},
+			validate.EnumOpt{
+				Name:  "N 2",
+				Value: 2.0,
+			},
+		),
+	)
+
+	b, err := json.MarshalIndent(fld, "", "  ")
+	require.NoError(t, err)
+	//fmt.Println(string(b))
+
+	res := field.NewField(nil)
+	err = json.Unmarshal(b, res)
+	require.NoError(t, err)
+
+	assert.Equal(t, fld, res)
+}
+
+func TestSchema_JSON(t *testing.T) {
+	enumStr := field.String().AddOptions(
+		validate.Enum(
+			validate.EnumOpt{
+				Name:  "N 1",
+				Value: "n1",
+			}, validate.EnumOpt{
+				Name:  "N 2",
+				Value: "n2",
+			},
+		),
+	).SetAdditionalValues()
+	enumInt := field.Number(field.NumberFormatFloat).AddOptions(
+		validate.Enum(
+			validate.EnumOpt{
+				Name:  "N 1",
+				Value: 1.1,
+			}, validate.EnumOpt{
+				Name:  "N 2",
+				Value: 2.5,
+			},
+		),
+	)
+	sch := schema.New(
+		"stringField", field.String().WithUI(&field.UI{Placeholder: "Test name"}).AddOptions(modify.TrimSpace()).AddOptions(validate.MinLength(2), validate.MaxLength(10)),
+		"stringField2", field.String(modify.Default("default")),
+		"intField", field.Number("int", validate.Required()),
+		"floatField", field.Number("float").SetIndexed(true),
+		"enumStringField", enumStr,
+		"enumIntField", enumInt,
+		"timeField", field.Time().SetSingleLocale(true),
+		"arrayField", field.Array(field.String(modify.Default("default"))),
+		"objectField", field.Object("innerStringField", field.String()).WithIncludes("ref1", field.Include{Ref: "ref2", Optional: true}),
+		"evaluatedField", field.String(modify.Value("stringField2 + '_' 	")),
+	)
+	sch.Loaded = true
+
+	b, err := json.MarshalIndent(sch, "", "  ")
+	require.NoError(t, err)
+	//fmt.Println(string(b))
+
+	res := schema.New()
+	err = json.Unmarshal(b, res)
+	require.NoError(t, err)
+
+	assert.Equal(t, sch, res)
+}
+
+func TestSchemaUI_UnmarshalJSON(t *testing.T) {
+	vw := &field.View{
+		Widget:  "Widget",
+		Options: map[string]interface{}{"title": "name", "key": "name"},
+	}
+	ui := &field.UI{
+		Widget:      "Widget",
+		Placeholder: "Placeholder",
+		Options:     map[string]interface{}{"title": "name", "key": "name"},
+		ListView:    vw,
+		ReadView:    vw,
+		EditView:    vw,
+	}
+	schm := schema.New(
+		"name", field.String().WithUI(ui),
+	)
+	schm.UI = ui
+
+	j := `{
+  "ui": {
+    "widget": "Widget",
+    "placeholder": "Placeholder",
+    "options": {
+      "title": "name",
+      "key": "name"
+    },
+    "read_view": {
+      "widget": "Widget",
+      "options": {
+        "title": "name",
+        "key": "name"
+      }
+    },
+    "edit_view": {
+      "widget": "Widget",
+      "options": {
+        "title": "name",
+        "key": "name"
+      }
+    },
+    "list_view": {
+      "widget": "Widget",
+      "options": {
+        "title": "name",
+        "key": "name"
+      }
+    }
+  },
+  "type": "object",
+  "params": {
+    "inline": false,
+    "fields": {
+      "name": {
+        "ui": {
+          "widget": "Widget",
+          "placeholder": "Placeholder",
+          "options": {
+            "title": "name",
+            "key": "name"
+          },
+          "read_view": {
+            "widget": "Widget",
+            "options": {
+              "title": "name",
+              "key": "name"
+            }
+          },
+          "edit_view": {
+            "widget": "Widget",
+            "options": {
+              "title": "name",
+              "key": "name"
+            }
+          },
+          "list_view": {
+            "widget": "Widget",
+            "options": {
+              "title": "name",
+              "key": "name"
+            }
+          }
+        },
+        "type": "string",
+        "params": {}
+      }
+    }
+  },
+  "loaded": false
+}`
+
+	sch := schema.New()
+	err := sch.UnmarshalJSON([]byte(j))
+	require.NoError(t, err)
+	assert.Equal(t, sch, schm)
+}
+
+func TestSchema_GetField(t *testing.T) {
+
+	sch := schema.New(
+		"str", field.String(),
+		"num", field.Number(field.NumberFormatInt),
+		"obj", field.Object(
+			"bool", field.Bool(),
+			"arr", field.Array(field.Time()),
+			"list", field.Array(
+				field.Object(
+					"num1", field.Number(field.NumberFormatFloat),
+					"str1", field.String(),
+					"obj1", field.Object(
+						"str2", field.String(),
+					),
+				),
+			),
+			"geo", field.Location(),
+		),
+	)
+
+	data := []struct {
+		fld    string
+		exists bool
+		typ    string
+	}{
+		{"str", true, "string"},
+		{"obj.bool", true, "bool"},
+		{"obj.list.num1", true, "number"},
+		{"obj.list.obj1.str2", true, "string"},
+		{"obj_list", false, ""},
+		{"zzz", false, ""},
+		{"obj.geo", true, "location"},
+	}
+
+	for _, d := range data {
+		t.Run(d.fld, func(t *testing.T) {
+			f := sch.GetField(d.fld)
+			if d.exists {
+				require.NotNil(t, f, fmt.Sprintf("not found '%s'", d.fld))
+				assert.Equal(t, d.typ, f.GetType().Name(), fmt.Sprintf("field '%s'", d.fld))
+			} else {
+				require.Nil(t, f)
+			}
+		})
+	}
+}
+
+func TestSchema_GetField_WithInline(t *testing.T) {
+
+	sch := schema.New(
+		"str", field.String(),
+		"obj1", field.Object(
+			true,
+			"obj11", field.Object(
+				true,
+				"obj111", field.Object(
+					true,
+					"str1", field.String(),
+					"str2", field.String(),
+				),
+				"arr1", field.Array(field.Object(
+					"str3", field.String(),
+				)),
+				"arr2", field.Array(field.String()),
+			),
+		),
+		"obj2", field.Object(
+			true,
+			"a", field.String(),
+			"b", field.String(),
+		),
+		"zz", field.Object(
+			true,
+			"zz", field.Array(field.Object(
+				"str3", field.String(),
+			)),
+		),
+	)
+
+	data := []struct {
+		fld    string
+		exists bool
+		typ    string
+	}{
+		{"str", true, "string"},
+		{"a", true, "string"},
+		{"b", true, "string"},
+		{"str1", true, "string"},
+		{"str2", true, "string"},
+		{"arr1", true, "array"},
+		{"arr2", true, "array"},
+		{"arr1.str3", true, "string"},
+		{"zz.str3", true, "string"},
+	}
+
+	for _, d := range data {
+		t.Run(d.fld, func(t *testing.T) {
+			f := sch.GetField(d.fld)
+			if d.exists {
+				require.NotNil(t, f, fmt.Sprintf("not found '%s'", d.fld))
+				assert.Equal(t, d.typ, f.GetType().Name(), fmt.Sprintf("field '%s'", d.fld))
+			} else {
+				require.Nil(t, f)
+			}
+		})
+	}
+}
+
+func TestSchema_GetFields(t *testing.T) {
+	sch := schema.New(
+		"str", field.String().SetTitle("Str"),
+		"num", field.Number(field.NumberFormatInt).SetIndexed(true).SetTitle("Num"),
+		"obj", field.Object(
+			"arr", field.Array(
+				field.Array(field.Time()).SetIndexed(true).SetTitle("NestedArr"),
+			).SetTitle("Arr"),
+			"list", field.Array(
+				field.Object(
+					"obj3", field.Object(
+						"str", field.String().SetIndexed(true).SetTitle("Obj2.List.Str"),
+					).SetTitle("Obj3"),
+				).SetTitle("Obj2"),
+			).SetTitle("List"),
+			"geo", field.Location().SetTitle("Geo"),
+		).SetTitle("Obj"),
+	)
+
+	flds := sch.GetFields(func(f *field.Field, path string) bool { return true })
+	assert.Len(t, flds, 8)
+
+	paths := make([]string, 0, len(flds))
+
+	for _, fld := range flds {
+		switch fld.Path {
+		case "str":
+			assert.IsType(t, &field.StringParameters{}, fld.Params)
+		case "num":
+			assert.IsType(t, &field.NumberParameters{}, fld.Params)
+		case "obj":
+			assert.IsType(t, &field.ObjectParameters{}, fld.Params)
+		case "obj.arr":
+			assert.IsType(t, &field.ArrayParameters{}, fld.Params)
+		case "obj.list":
+			assert.IsType(t, &field.ArrayParameters{}, fld.Params)
+		case "obj.list.obj3":
+			assert.IsType(t, &field.ObjectParameters{}, fld.Params)
+		case "obj.list.obj3.str":
+			assert.IsType(t, &field.StringParameters{}, fld.Params)
+		case "obj.geo":
+			assert.IsType(t, &field.LocationParameters{}, fld.Params)
+
+		}
+
+		paths = append(paths, fld.Path)
+	}
+
+	assert.ElementsMatch(
+		t,
+		[]string{"str", "num", "obj", "obj.arr", "obj.list", "obj.list.obj3", "obj.list.obj3.str", "obj.geo"},
+		paths,
+	)
+
+}
+
+func TestSchema_GetFieldByPath(t *testing.T) {
+	sch := schema.New(
+		"str", field.String().SetTitle("Str"),
+		"num", field.Number(field.NumberFormatInt).SetIndexed(true).SetTitle("Num"),
+		"obj", field.Object(
+			"arr", field.Array(
+				field.Array(field.Time()).SetIndexed(true).SetTitle("NestedArr"),
+			).SetTitle("Arr"),
+			"list", field.Array(
+				field.Object(
+					"obj3", field.Object(
+						"str", field.String().SetIndexed(true).SetTitle("Obj2.List.Str"),
+					).SetTitle("Obj3"),
+				).SetTitle("Obj2"),
+			).SetTitle("List"),
+			"geo", field.Location().SetTitle("Geo"),
+		).SetTitle("Obj"),
+	)
+
+	dt := []struct {
+		name  string
+		paths []string
+		want  []string
+	}{
+		{
+			"all",
+			[]string{"*"},
+			[]string{"str", "num", "obj", "obj.arr", "obj.list", "obj.list.obj3", "obj.list.obj3.str", "obj.geo"},
+		},
+		{
+			"full match",
+			[]string{"str", "obj.list.obj3", "some"},
+			[]string{"str", "obj.list.obj3"},
+		},
+		{
+			"glob",
+			[]string{"str*", "obj.list*", "*geo"},
+			[]string{"str", "obj.list", "obj.list.obj3", "obj.list.obj3.str", "obj.geo"},
+		},
+	}
+
+	for _, d := range dt {
+		t.Run(d.name, func(t *testing.T) {
+			got := field.GetFieldsPath(sch.GetFields(func(f *field.Field, path string) bool {
+				return data.GlobMatch(path, d.paths...)
+			}))
+			assert.ElementsMatch(t, d.want, got)
+		})
+	}
+}
+
+func TestSchema_GetFieldsInline(t *testing.T) {
+	t.Run("Basic", func(t *testing.T) {
+		sch := schema.New(
+			"str_1", field.String(),
+			"num", field.Number(field.NumberFormatInt).SetIndexed(true),
+			"obj_1", field.Object(
+				"arr", field.Array(field.Time()).SetIndexed(true),
+				"list", field.Array(
+					field.Object(
+						"obj_2", field.Object(
+							"str_2", field.String().SetIndexed(true),
+						),
+					),
+				),
+				"geo", field.Location(),
+			),
+		)
+
+		flds := field.GetFieldsPath(sch.GetFields(func(f *field.Field, p string) bool {
+			return true
+		}, "data"))
+		assert.ElementsMatch(
+			t,
+			[]string{
+				"data",
+				"data.str_1",
+				"data.num",
+				"data.obj_1",
+				"data.obj_1.arr",
+				"data.obj_1.list",
+				"data.obj_1.list.obj_2",
+				"data.obj_1.list.obj_2.str_2",
+				"data.obj_1.geo",
+			},
+			flds,
+		)
+	})
+	t.Run("Inline fields in schema in a row", func(t *testing.T) {
+		sch := schema.New(
+			"obj_inline_1", field.Object(
+				true,
+				"inline_field1", field.String().SetUnique(true),
+				"obj_inline_2", field.Object(true,
+					"inline_field2", field.String(),
+					"arr", field.Array(field.Object(true,
+						"inline_field3", field.String(),
+					)),
+				),
+			),
+		)
+
+		flds := field.GetFieldsPath(sch.GetFields(func(f *field.Field, p string) bool {
+			return true
+		}))
+		assert.ElementsMatch(
+			t,
+			[]string{
+				"obj_inline_1",
+				"inline_field1",
+				"obj_inline_2",
+				"inline_field2",
+				"arr",
+				"arr.inline_field3",
+			},
+			flds,
+		)
+	})
+	t.Run("Inline fields in schema in a row with prefix", func(t *testing.T) {
+		sch := schema.New(
+			"obj_inline_1", field.Object(true,
+				"inline_field1", field.String().SetUnique(true),
+				"obj_inline_2", field.Object(true,
+					"inline_field2", field.String(),
+					"obj_inline_3", field.Object(true,
+						"inline_field3", field.String(),
+					),
+				),
+			),
+		)
+
+		flds := field.GetFieldsPath(sch.GetFields(func(f *field.Field, p string) bool {
+			return true
+		}, "data"))
+		assert.ElementsMatch(
+			t,
+			[]string{
+				"data",
+				"data.obj_inline_1",
+				"data.inline_field1",
+				"data.obj_inline_2",
+				"data.inline_field2",
+				"data.obj_inline_3",
+				"data.inline_field3",
+			},
+			flds,
+		)
+	})
+	t.Run("Mixed fields in schema in a row", func(t *testing.T) {
+		sch := schema.New(
+			"obj_not_inline_1", field.Object(
+				"not_inline_field_1", field.String().SetUnique(true),
+				"obj_inline_1", field.Object(true,
+					"inline_field1", field.String(),
+					"obj_not_inline_2", field.Object(
+						"not_inline_field_2", field.String(),
+						"obj_inline_2", field.Object(true,
+							"inline_field2", field.String(),
+						),
+					),
+				),
+			),
+		)
+
+		flds := field.GetFieldsPath(sch.GetFields(func(f *field.Field, p string) bool {
+			return true
+		}, "data"))
+		assert.ElementsMatch(
+			t,
+			[]string{
+				"data",
+				"data.obj_not_inline_1",
+				"data.obj_not_inline_1.not_inline_field_1",
+				"data.obj_not_inline_1.obj_inline_1",
+				"data.obj_not_inline_1.inline_field1",
+				"data.obj_not_inline_1.obj_not_inline_2",
+				"data.obj_not_inline_1.obj_not_inline_2.not_inline_field_2",
+				"data.obj_not_inline_1.obj_not_inline_2.obj_inline_2",
+				"data.obj_not_inline_1.obj_not_inline_2.inline_field2",
+			},
+			flds,
+		)
+	})
+}
+
+func TestSchema_Clone(t *testing.T) {
+	sch := schema.New(
+		"f", field.String().WithUI(&field.UI{Placeholder: "Test name"}).AddOptions(modify.TrimSpace()).AddTranslation("ru", "ф", "Поле Ф"),
+		"obj", field.Object(
+			"list", field.Array(
+				field.Object(
+					"obj", field.Object(
+						"field", field.String(),
+					),
+				),
+			),
+		),
+	)
+
+	t.Run("Simple", func(t *testing.T) {
+		f := sch.GetField("f")
+		fld := f.Clone(false)
+
+		assert.Equal(t, f.UI, fld.UI)
+		assert.Equal(t, f.Options, fld.Options)
+		assert.Equal(t, f.Translations, fld.Translations)
+		assert.Equal(t, f.Params, fld.Params)
+	})
+
+	t.Run("Reset", func(t *testing.T) {
+		f := sch.GetField("obj")
+		fld := f.Clone(true)
+
+		assert.Equal(t, f.UI, fld.UI)
+		assert.Equal(t, f.Options, fld.Options)
+		assert.Equal(t, f.Translations, fld.Translations)
+		assert.NotEqual(t, f.Params, fld.Params)
+
+		f = sch.GetField("obj.list")
+		fld = f.Clone(true)
+
+		assert.Equal(t, f.UI, fld.UI)
+		assert.Equal(t, f.Options, fld.Options)
+		assert.Equal(t, f.Translations, fld.Translations)
+		assert.NotEqual(t, f.Params, fld.Params)
+
+		f = sch.GetField("obj.list.obj")
+		fld = f.Clone(true)
+
+		assert.Equal(t, f.UI, fld.UI)
+		assert.Equal(t, f.Options, fld.Options)
+		assert.Equal(t, f.Translations, fld.Translations)
+		assert.NotEqual(t, f.Params, fld.Params)
+	})
+}
+
+func TestSchema_Modify(t *testing.T) {
+	sch := schema.New(
+		"name", field.String(validate.Required()),
+		"last_name", field.String(validate.Required()),
+		"got_nobel", field.Bool(),
+		"times", field.Number("int"),
+		"dates", field.Array(field.Time()),
+	)
+
+	in := map[string]interface{}{"last_name": "Curie", "name": "Marie"}
+	_, _, err := modify.Modify(nil, sch, in)
+	require.NoError(t, err)
+}
+
+func TestSchema_Validate(t *testing.T) {
+	sch := schema.New(
+		"name", field.String(validate.Required()),
+		"last_name", field.String(),
+		"info", field.Object(
+			"time", field.Time(),
+			"numbers", field.Number(
+				field.NumberFormatInt,
+				validate.Enum(
+					validate.EnumOpt{Name: "first", Value: 1},
+					validate.EnumOpt{Name: "second", Value: 2},
+				),
+			),
+		),
+	)
+
+	in := map[string]interface{}{"info": map[string]interface{}{"time": time.Now()}, "name": "Name"}
+	err := validate.Validate(nil, sch, in)
+	require.NoError(t, err)
+}
+
+func TestSchema_DecodeErrors(t *testing.T) {
+	sch := schema.New(
+		"name", field.String(validate.Required()),
+		"last_name", field.String(),
+		"a", field.Object(
+			"time", field.Time(),
+			"num1", field.Number(field.NumberFormatInt),
+			"num2", field.Number(field.NumberFormatInt),
+			"num3", field.Number(field.NumberFormatInt),
+			"b", field.Object(
+				"num1", field.Number(field.NumberFormatInt),
+				"num2", field.Number(field.NumberFormatInt),
+				"num3", field.Number(field.NumberFormatInt),
+			),
+			"c", field.Array(field.Number(field.NumberFormatInt)),
+			"d", field.Number(field.NumberFormatInt, validate.Max(10)),
+		),
+	)
+
+	in := map[string]interface{}{"a": map[string]interface{}{"time": time.Now(), "num1": "a", "num2": "b", "num3": "c", "d": 20,
+		"b": map[string]interface{}{"time": time.Now(), "num1": "a", "num2": "b", "num3": "c", "str": "s"}, "c": []interface{}{"a", "b", "c"}},
+		"name": "Name"}
+	_, err := schema.Decode(nil, sch, in)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "decode error")
+}
+
+func TestSchema_ValidateErrors(t *testing.T) {
+	sch := schema.New(
+		"a", field.Object(
+			"num1", field.Number(field.NumberFormatInt, validate.Required()),
+			"num2", field.Number(field.NumberFormatInt, validate.Max(10)),
+			"num3", field.Number(field.NumberFormatInt, validate.Min(10)),
+			"str1", field.String(validate.MaxLength(5)),
+			"str2", field.String(validate.MinLength(5)),
+			"str3", field.String(validate.MinLength(5), validate.Enum(validate.EnumOpt{Value: "somesome"}, validate.EnumOpt{Value: "romoromo"})),
+		),
+	)
+
+	in := map[string]interface{}{"a": map[string]interface{}{"num2": 20, "num3": 5, "str1": "123456", "str2": "123", "str3": "some"}}
+	decoded, err := schema.Decode(nil, sch, in)
+	require.NoError(t, err)
+	err = validate.Validate(nil, sch, decoded)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "validation error")
+	var merr *multierror.Error
+	require.ErrorAs(t, err, &merr)
+	assert.Len(t, merr.Errors, 6)
+}
+
+func TestSchema_ValidateEmptyObject(t *testing.T) {
+	{
+		sch := schema.New(
+			"num1", field.Number(field.NumberFormatInt, validate.Required()),
+		)
+
+		res, err := schema.Decode(nil, sch, nil)
+		require.NoError(t, err)
+		res, _, err = modify.Modify(nil, sch, res)
+		require.NoError(t, err)
+		err = validate.Validate(nil, sch, res)
+		require.NoError(t, err, "поля объекта nil не проверяются")
+	}
+	{
+		sch := schema.New(
+			"num1", field.Number(field.NumberFormatInt, validate.Required()),
+		)
+
+		res, err := schema.Decode(nil, sch, map[string]interface{}{})
+		require.NoError(t, err)
+		res, _, err = modify.Modify(nil, sch, res)
+		require.NoError(t, err)
+		err = validate.Validate(nil, sch, res)
+		require.Error(t, err, "поля пустого объекта проверяются")
+	}
+	{
+		sch := schema.New(
+			"num1", field.Number(field.NumberFormatInt, validate.Required()),
+		)
+
+		res, err := schema.Decode(nil, sch, map[string]interface{}{"a": "sss"})
+		require.NoError(t, err)
+		res, _, err = modify.Modify(nil, sch, res)
+		require.NoError(t, err)
+		err = validate.Validate(nil, sch, res)
+		require.Error(t, err, "поля объекта с некорректными данными проверяются")
+	}
+
+	{
+		sch := schema.New(
+			"num1", field.Number(field.NumberFormatInt, validate.Required()),
+		).AddOptions(modify.Default(map[string]interface{}{}))
+
+		res, err := schema.Decode(nil, sch, nil)
+		require.NoError(t, err)
+		res, _, err = modify.Modify(nil, sch, res)
+		require.NoError(t, err)
+		err = validate.Validate(nil, sch, res)
+		require.Error(t, err, "поля nil объекта Default данными проверяются")
+	}
+}
+
+func TestSchema_ModificationErrors(t *testing.T) {
+	sch := schema.New(
+		"a", field.Object(
+			"num1", field.Number(field.NumberFormatInt, modify.TrimSpace()),
+			"str1", field.String(modify.TrimSpace()),
+		),
+	)
+
+	in := map[string]interface{}{"a": map[string]interface{}{"num1": 20, "num3": 5, "str1": "123456", "str2": "123", "str3": "some"}}
+	decoded, err := schema.Decode(nil, sch, in)
+	require.NoError(t, err)
+	_, _, err = modify.Modify(nil, sch, decoded)
+	require.Error(t, err)
+	require.Contains(t, err.Error(), "modification error")
+	var merr *multierror.Error
+	require.ErrorAs(t, err, &merr)
+	assert.Len(t, merr.Errors, 1)
+}
+
+func TestSchema_UnknownJSON(t *testing.T) {
+	sch := schema.New(
+		"name", field.String(validate.Required()),
+		"last_name", field.String(validate.Required()),
+		"got_nobel", field.Bool(),
+		"times", field.Number("int"),
+		"dates", field.Array(field.Time()),
+	)
+
+	b, err := json.Marshal(sch)
+	require.NoError(t, err)
+	field.Unregister("object")
+
+	s1 := schema.New()
+	err = json.Unmarshal(b, s1)
+	require.NoError(t, err)
+	assert.Equal(t, "unknown", s1.GetType().Name(), "Схема неизвестного типа должна определяться как unknown")
+
+	in := map[string]interface{}{"info": map[string]interface{}{"time": time.Now()}, "name": "Name"}
+	out, err := field.Decode(nil, s1, in)
+	require.NoError(t, err)
+	assert.Equal(t, in, out, "Данные неизвестного типа не изменяются при декодировании")
+	err = validate.Validate(nil, s1, in)
+	require.NoError(t, err, "Данные неизвестного типа не валидируются вглубь")
+
+	b, err = json.Marshal(s1)
+	require.NoError(t, err)
+	s2 := schema.New()
+	err = json.Unmarshal(b, s2)
+	require.NoError(t, err)
+	b, err = json.Marshal(s2)
+	require.NoError(t, err)
+	assert.Equal(t, "unknown", s2.GetType().Name(), "Схема неизвестного типа должна определяться как unknown")
+	assert.Equal(t, s1, s2, "Схема не должна меняться при повторном маршалинге")
+
+	field.Register(&field.ObjectType{})
+	s3 := schema.New()
+	err = json.Unmarshal(b, s3)
+	require.NoError(t, err)
+	assert.Equal(t, "object", s3.GetType().Name(), "Схема должна восстановить тип object при восстановление регистрации типа")
+	assert.Equal(t, sch, s3, "Схема должна восстановиться при восстановление регистрации типа")
+}
+
+func TestSchema_ValidOptions(t *testing.T) {
+	t.Run("Valid Options", func(t *testing.T) {
+		schm := `{
+	"type": "object",
+	"params": {
+		"fields": {
+			"required": {
+				"options": {
+					"required": true
+				},
+				"type": "string"
+			},
+			"readonly": {
+				"options": {
+					"readonly": true
+				},
+				"type": "string"
+			},
+			"enum": {
+				"options": {
+					"enum": [{
+							"name": "One",
+							"value": "one"
+						},
+						{
+							"name": "Two",
+							"value": "two"
+						}
+					]
+				},
+				"type": "string"
+			}
+		}
+	}
+}`
+
+		s := schema.New()
+		err := json.Unmarshal([]byte(schm), s)
+		require.NoError(t, err)
+
+		required := s.GetField("required")
+		readonly := s.GetField("readonly")
+		enum := s.GetField("enum")
+
+		require.NotEmpty(t, required.Options)
+		require.NotEmpty(t, readonly.Options)
+		require.NotEmpty(t, enum.Options)
+	})
+
+	t.Run("Invalid Options", func(t *testing.T) {
+		schm := `{
+	"type": "object",
+	"params": {
+		"fields": {
+			"required": {
+				"options": {
+					"required": false
+				},
+				"type": "string"
+			},
+			"readonly": {
+				"options": {
+					"readonly": false
+				},
+				"type": "string"
+			}
+		}
+	}
+}`
+
+		s := schema.New()
+		err := json.Unmarshal([]byte(schm), s)
+		require.NoError(t, err)
+
+		required := s.GetField("required")
+		readonly := s.GetField("readonly")
+
+		require.Empty(t, required.Options)
+		require.Empty(t, readonly.Options)
+	})
+
+	t.Run("Required Enum Name", func(t *testing.T) {
+		schm := `{
+	"type": "object",
+	"params": {
+		"fields": {
+			"enum": {
+				"options": {
+					"enum": [{
+							"value": "one"
+						},
+						{
+							"value": "two"
+						}
+					]
+				},
+				"type": "string"
+			}
+		}
+	}
+}`
+		s := schema.New()
+		err := json.Unmarshal([]byte(schm), s)
+		require.Error(t, err)
+		assert.Contains(t, err.Error(), "enum name is required")
+	})
+}
+
+func TestSchema_Condition(t *testing.T) {
+	sch := schema.New(
+		"type", field.String(modify.TrimSpace()),
+		"a", field.Number(field.NumberFormatInt).SetCondition("type contains 'a'"),
+		"b", field.Number(field.NumberFormatInt, validate.Required()).SetCondition("type contains 'b'"),
+		"c", field.Number(field.NumberFormatInt).SetCondition("a==10"),
+		"obj", field.Object(
+			"a", field.Number(field.NumberFormatInt).SetCondition("type contains 'a'"),
+			"b", field.Number(field.NumberFormatInt).SetCondition("type contains 'b'"),
+			"c", field.Number(field.NumberFormatInt).SetCondition("_.a < 10"),
+			"d", field.Number(field.NumberFormatInt, modify.Default(11)).SetCondition("_.a < 10"),
+		),
+		"obj3", field.Object(
+			"fld1", field.Number(field.NumberFormatInt),
+		).SetCondition("obj.d > 10"),
+	)
+
+	tests := []struct {
+		name    string
+		data    map[string]interface{}
+		want    map[string]interface{}
+		wantErr bool
+	}{
+		{"type a",
+			map[string]interface{}{
+				"type": "a",
+				"a":    int64(10),
+				"b":    int64(10),
+				"c":    int64(1),
+				"obj":  map[string]interface{}{"a": int64(1), "b": int64(20), "c": int64(11), "d": int64(11)},
+				"obj3": map[string]interface{}{"fld1": int64(6)},
+			},
+			map[string]interface{}{
+				"type": "a",
+				"a":    int64(10),
+				"c":    int64(1),
+				"obj":  map[string]interface{}{"a": int64(1), "c": int64(11), "d": int64(11)},
+				"obj3": map[string]interface{}{"fld1": int64(6)},
+			},
+			false},
+		{"type b",
+			map[string]interface{}{
+				"type": "b",
+				"a":    int64(10),
+				"b":    int64(10),
+				"c":    int64(1),
+				"obj":  map[string]interface{}{"a": int64(1), "b": int64(20), "c": int64(11), "d": int64(11)},
+				"obj3": map[string]interface{}{"fld1": int64(6)},
+			},
+			map[string]interface{}{
+				"type": "b",
+				"b":    int64(10),
+				"obj":  map[string]interface{}{"b": int64(20)},
+			},
+			false},
+		{"type ab + default",
+			map[string]interface{}{
+				"type": " ab  ",
+				"a":    int64(1),
+				"b":    int64(10),
+				"c":    int64(1),
+				"obj":  map[string]interface{}{"a": int64(1), "b": int64(20), "c": int64(11)},
+				"obj3": map[string]interface{}{"fld1": int64(6)},
+			},
+			map[string]interface{}{
+				"type": "ab",
+				"a":    int64(1),
+				"b":    int64(10),
+				"obj":  map[string]interface{}{"a": int64(1), "b": int64(20), "c": int64(11), "d": int64(11)},
+				"obj3": map[string]interface{}{"fld1": int64(6)},
+			},
+			false},
+	}
+
+	ctx := context.Background()
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := sch.ToValue(ctx, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+			assert.Equal(t, tt.want, got)
+		})
+	}
+
+}
+
+func TestSchema_Inline(t *testing.T) {
+	sch := schema.New(
+		"a", field.String(),
+		"b", field.String(),
+		"obj", field.Object(
+			true,
+			"c", field.String(),
+			"d", field.Number(field.NumberFormatInt),
+			"inner_obj", field.Object(
+				true,
+				"f", field.String(),
+			),
+		).SetCondition("a == 'universe'"),
+		"overlap", field.Object(
+			"obj1", field.Object(
+				true,
+				"f1", field.Number(field.NumberFormatInt),
+				"f2", field.String(),
+			),
+			"obj2", field.Object(
+				true,
+				"f1", field.Number(field.NumberFormatInt),
+				"f2", field.Number(field.NumberFormatInt),
+			),
+		),
+		"arr", field.Array(
+			field.Object(
+				true,
+				"x", field.String(),
+			),
+		),
+	)
+
+	tests := []struct {
+		name    string
+		data    map[string]interface{}
+		want    map[string]interface{}
+		wantErr bool
+	}{
+		{"Condition success",
+			map[string]interface{}{"a": "universe", "b": "life", "c": "everything", "d": int64(42)},
+			map[string]interface{}{"a": "universe", "b": "life", "c": "everything", "d": int64(42)},
+			false,
+		},
+		{"Condition fail",
+			map[string]interface{}{"a": "life", "b": "universe", "c": "everything", "d": int64(42)},
+			map[string]interface{}{"a": "life", "b": "universe"},
+			false,
+		},
+		{"Condition success, level 2 inline",
+			map[string]interface{}{"a": "universe", "b": "life", "c": "everything", "d": int64(42), "f": "some"},
+			map[string]interface{}{"a": "universe", "b": "life", "c": "everything", "d": int64(42), "f": "some"},
+			false,
+		},
+		{"Condition fail, level 2 inline",
+			map[string]interface{}{"a": "life", "b": "universe", "c": "everything", "d": int64(42), "f": "some"},
+			map[string]interface{}{"a": "life", "b": "universe"},
+			false,
+		},
+		{"Overlapped",
+			map[string]interface{}{"overlap": map[string]interface{}{"f1": 42}},
+			map[string]interface{}{"overlap": map[string]interface{}{"f1": int64(42)}},
+			false,
+		},
+		{"Overlapped, type conflict",
+			map[string]interface{}{"overlap": map[string]interface{}{"f1": 42, "f2": "everything"}},
+			nil,
+			true,
+		},
+		{"Array, ignore inline",
+			map[string]interface{}{"a": "life", "b": "universe", "c": "everything", "d": int64(42), "x": "some", "arr": []interface{}{map[string]interface{}{"x": "some"}}},
+			map[string]interface{}{"a": "life", "b": "universe", "arr": []interface{}{map[string]interface{}{"x": "some"}}},
+			false,
+		},
+	}
+
+	ctx := context.Background()
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := sch.ToValue(ctx, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
+
+func TestSchema_Introspect(t *testing.T) {
+	tests := []struct {
+		name           string
+		data           map[string]interface{}
+		schema         *schema.Schema
+		want           map[string]interface{}
+		wantFields     []string
+		dontWantFields []string
+		wantErr        bool
+	}{
+		{"single true condition",
+			map[string]interface{}{
+				"a": "b",
+				"b": "b",
+			},
+			schema.New(
+				"a", field.String(),
+				"b", field.String().SetCondition("a == 'b'"),
+			),
+			map[string]interface{}{
+				"a": "b",
+				"b": "b",
+			},
+			[]string{"a", "b"},
+			[]string{},
+			false},
+		{"single false condition",
+			map[string]interface{}{
+				"a": "a",
+				"b": "b",
+			},
+			schema.New(
+				"a", field.String(),
+				"b", field.String().SetCondition("a == 'b'"),
+			),
+			map[string]interface{}{
+				"a": "a",
+			},
+			[]string{"a"},
+			[]string{"b"},
+			false},
+		{"multiple true conditions",
+			map[string]interface{}{
+				"a": "a",
+				"b": "b",
+				"c": "c",
+				"d": "d",
+			},
+			schema.New(
+				"a", field.String(),
+				"b", field.String().SetCondition("a == 'a'"),
+				"c", field.String().SetCondition("b == 'b'"),
+				"d", field.String().SetCondition("c == 'c'"),
+			),
+			map[string]interface{}{
+				"a": "a",
+				"b": "b",
+				"c": "c",
+				"d": "d",
+			},
+			[]string{"a", "b", "c", "d"},
+			[]string{},
+			false},
+		{"multiple conditions some true",
+			map[string]interface{}{
+				"a": "a",
+				"b": "bb",
+				"c": "c",
+				"d": "d",
+			},
+			schema.New(
+				"a", field.String(),
+				"b", field.String().SetCondition("a == 'a'"),
+				"c", field.String().SetCondition("b == 'b'"),
+				"d", field.String().SetCondition("c == 'c'"),
+			),
+			map[string]interface{}{
+				"a": "a",
+				"b": "bb",
+			},
+			[]string{"a", "b"},
+			[]string{"c", "d"},
+			false},
+		{"nil data",
+			nil,
+			schema.New(
+				"a", field.String(),
+				"b", field.String(),
+			),
+			nil,
+			[]string{"a", "b"},
+			nil,
+			false},
+		{"empty data",
+			map[string]interface{}{},
+			schema.New(
+				"a", field.String(),
+				"b", field.String(),
+			),
+			map[string]interface{}{},
+			[]string{"a", "b"},
+			nil,
+			false},
+		{"data with other fields",
+			map[string]interface{}{"some": "some"},
+			schema.New(
+				"a", field.String(),
+				"b", field.String(),
+			),
+			map[string]interface{}{},
+			[]string{"a", "b"},
+			nil,
+			false},
+		{"nil object",
+			map[string]interface{}{"a": "aa"},
+			schema.New(
+				"a", field.String(),
+				"j", field.Object(
+					"aa", field.String(),
+					"bb", field.Number(field.NumberFormatInt),
+				),
+			),
+			map[string]interface{}{"a": "aa"},
+			[]string{"a", "j", "j.aa", "j.bb"},
+			nil,
+			false},
+		{
+			"object condition",
+			map[string]interface{}{"key": "a", "object_b": map[string]interface{}{"field1": "a", "field2": "a"}},
+			schema.New(
+				"key", field.String(modify.Default("default")),
+				"object_b", field.Object(
+					"field1", field.String(),
+					"field2", field.String(),
+				),
+				"object_a", field.Object(
+					"field1", field.String(),
+					"field2", field.String(),
+				).SetCondition("key=='a'"),
+			),
+			map[string]interface{}{"key": "a", "object_b": map[string]interface{}{"field1": "a", "field2": "a"}},
+			[]string{"key", "object_b", "object_a", "object_b.field1", "object_b.field2"},
+			[]string{"field1", "field2"},
+			false,
+		},
+		{
+			"object condition with nil data",
+			nil,
+			schema.New(
+				"key", field.String(modify.Default("default")),
+				"object_b", field.Object(
+					"field1", field.String(),
+					"field2", field.String(),
+				),
+				"object_a", field.Object(
+					"field1", field.String(),
+					"field2", field.String(),
+				).SetCondition("key=='a'"),
+			),
+			nil,
+			[]string{"key", "object_b", "object_b.field1", "object_b.field2"},
+			[]string{"object_a", "field1", "field2"},
+			false,
+		},
+	}
+
+	ctx := context.Background()
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotValue, gotSchema, err := tt.schema.Introspect(ctx, tt.data)
+			require.NoError(t, err)
+
+			if tt.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+
+			for _, f := range tt.wantFields {
+				fld := gotSchema.GetField(f)
+				assert.NotNil(t, fld, fmt.Sprintf("поле '%s' должно присутствовать в схеме", f))
+			}
+
+			for _, f := range tt.dontWantFields {
+				fld := gotSchema.GetField(f)
+				assert.Nil(t, fld, fmt.Sprintf("поле '%s' должно отсутствовать в схеме", f))
+			}
+
+			//b, err := json.MarshalIndent(got.Schema, "", "  ")
+			//require.NoError(t, err)
+			//fmt.Printf("---\n%s\n---\n", b)
+			assert.Equal(t, tt.want, gotValue)
+		})
+	}
+
+}
+
+func TestSchema_IntrospectObjectArray(t *testing.T) {
+	tests := []struct {
+		name       string
+		data       map[string]interface{}
+		schema     *schema.Schema
+		want       map[string]interface{}
+		wantParams []string
+		wantErr    bool
+	}{
+		{
+			"simple",
+			map[string]interface{}{"array": []interface{}{map[string]interface{}{"field1": "a", "field2": "a"}}},
+			schema.New(
+				"array", field.Array(
+					field.Object(
+						"field1", field.String(),
+						"field2", field.String(),
+					),
+				)),
+			map[string]interface{}{"array": []interface{}{map[string]interface{}{"field1": "a", "field2": "a"}}},
+			[]string{"field1", "field2"},
+			false,
+		},
+		{
+			"empty data",
+			nil,
+			schema.New(
+				"array", field.Array(
+					field.Object(
+						"field1", field.String(),
+						"field2", field.String(),
+					),
+				)),
+			nil,
+			[]string{"field1", "field2"},
+			false,
+		},
+	}
+
+	ctx := context.Background()
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotValue, gotSchema, err := tt.schema.Introspect(ctx, tt.data)
+			require.NoError(t, err)
+
+			if tt.wantErr {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+
+			for _, f := range tt.wantParams {
+				fld := gotSchema.GetField("array")
+				p, ok := fld.Params.(*field.ArrayParameters).Item.Params.(*field.ObjectParameters)
+				assert.True(t, ok)
+				assert.Contains(t, p.Fields, f, fmt.Sprintf("поле '%s' должно присутствовать в параметрах Item", f))
+			}
+			assert.Equal(t, tt.want, gotValue)
+		})
+	}
+
+}
+
+func TestSchema_Load(t *testing.T) {
+	f1 := schema.New(
+		"f", field.String(),
+		"s3", field.Object("a1", field.Number(field.NumberFormatInt), "f", field.String()).WithIncludes("f2"),
+	)
+	f2 := schema.New("a", field.String())
+
+	sch := schema.NewFromField(field.Object(
+		"s1", field.String(),
+		"s2", field.String(),
+		"s3", field.Object("a1", field.String(), "a2", field.String()),
+		"s4", field.Array(field.Object().WithIncludes("f2")),
+	).WithIncludes("f1", "f2"),
+	)
+
+	loader := field.MultiLoader(
+		field.LoaderFunc(func(ref string) (fs []*field.Field, err error) {
+			if ref == "f1" {
+				f := f1.Field // copy
+				return []*field.Field{&f}, nil
+			}
+			return nil, fmt.Errorf("invalid schema reference: %s", ref)
+		}),
+		field.LoaderFunc(func(ref string) (fs []*field.Field, err error) {
+			if ref == "f2" {
+				f := f2.Field // copy
+				return []*field.Field{&f}, nil
+			}
+			return nil, fmt.Errorf("invalid schema reference: %s", ref)
+		}),
+	)
+	schema.SetDefaultLoader(loader)
+
+	err := sch.Load(nil)
+	require.NoError(t, err)
+
+	//b, _ := json.MarshalIndent(sch, "", "  ")
+	//fmt.Println(string(b))
+
+	assert.NotNil(t, sch.GetField("s1"))
+	assert.NotNil(t, sch.GetField("s2"))
+	assert.NotNil(t, sch.GetField("f"))
+	assert.NotNil(t, sch.GetField("a"))
+	assert.NotNil(t, sch.GetField("s3"))
+	assert.NotNil(t, sch.GetField("s3.f"))
+	assert.NotNil(t, sch.GetField("s3.a"))
+	{
+		f := sch.GetField("s3.a1")
+		require.NotNil(t, f)
+		assert.Equal(t, f.GetType(), &field.StringType{})
+	}
+	assert.NotNil(t, sch.GetField("s4.a"))
+
+}
+
+func TestSchema_WithIncludesCircle(t *testing.T) {
+	f1 := schema.New("f2", field.Object().WithIncludes("f2"))
+	f2 := schema.New("f3", field.Object().WithIncludes("f3"))
+	f3 := schema.New("f1", field.Object().WithIncludes("f1"))
+
+	loader := field.MultiLoader(
+		field.LoaderFunc(func(ref string) (fs []*field.Field, err error) {
+			if ref == "f1" {
+				f := f1.Field // copy
+				return []*field.Field{&f}, nil
+			}
+			return nil, fmt.Errorf("invalid schema reference: %s", ref)
+		}),
+		field.LoaderFunc(func(ref string) (fs []*field.Field, err error) {
+			if ref == "f2" {
+				f := f2.Field // copy
+				return []*field.Field{&f}, nil
+			}
+			return nil, fmt.Errorf("invalid schema reference: %s", ref)
+		}),
+		field.LoaderFunc(func(ref string) (fs []*field.Field, err error) {
+			if ref == "f3" {
+				f := f3.Field // copy
+				return []*field.Field{&f}, nil
+			}
+			return nil, fmt.Errorf("invalid schema reference: %s", ref)
+		}),
+	)
+	schema.SetDefaultLoader(loader)
+	sch := schema.NewFromField(field.Object().WithIncludes("f1"))
+
+	err := sch.Load(nil)
+	require.Error(t, err)
+	assert.EqualError(t, err, "limit for included fields exceeded")
+}
+
+func TestSchema_EnumUIOptions(t *testing.T) {
+	schm := `{
+	"type": "object",
+	"params": {
+		"fields": {
+			"enum": {
+				"options": {
+					"enum": [{
+							"name": "1",
+							"value": "one",
+							"ui" : {
+							  "color": "color",
+							  "icon": "icon",
+							  "spin": true,
+							  "blink": false		
+							}
+							},
+							{
+								"name": "2",
+								"value": "two"
+							}
+					]
+				},
+				"type": "string"
+			}
+		}
+	}
+}`
+	s := schema.New()
+	err := json.Unmarshal([]byte(schm), s)
+	require.NoError(t, err)
+}
diff --git a/pkg/schema/validate/array.go b/pkg/schema/validate/array.go
new file mode 100644
index 0000000000000000000000000000000000000000..09e17e289cdfeca4cbe743271b77300a4cb7a0d8
--- /dev/null
+++ b/pkg/schema/validate/array.go
@@ -0,0 +1,51 @@
+package validate
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+type maxItems int
+
+func MaxItems(max int) Validator {
+	v := maxItems(max)
+	return &v
+}
+
+func (t maxItems) Validate(_ context.Context, _ *field.Field, value interface{}) error {
+	if value == nil {
+		return nil
+	}
+	v := reflect.ValueOf(value)
+	if !v.IsValid() || v.Kind() != reflect.Array && v.Kind() != reflect.Slice {
+		return fmt.Errorf("incorrect type: \"%s\", expected \"array\"", v.Kind())
+	}
+	if t > 0 && v.Len() > int(t) {
+		return fmt.Errorf("maximum elements number is %d", t)
+	}
+	return nil
+}
+
+type minItems int
+
+func MinItems(min int) Validator {
+	v := minItems(min)
+	return &v
+}
+
+func (t minItems) Validate(_ context.Context, _ *field.Field, value interface{}) error {
+	if value == nil {
+		return nil
+	}
+	v := reflect.ValueOf(value)
+	if !v.IsValid() || v.Kind() != reflect.Array && v.Kind() != reflect.Slice {
+		return fmt.Errorf("incorrect type: \"%s\", expected \"array\"", v.Kind())
+	}
+	if t > 0 && v.Len() < int(t) {
+		return fmt.Errorf("minimum elements number is %d", t)
+	}
+	return nil
+}
diff --git a/pkg/schema/validate/array_test.go b/pkg/schema/validate/array_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..57b99445d4681b3994cb9cc5e3c35822a38a4900
--- /dev/null
+++ b/pkg/schema/validate/array_test.go
@@ -0,0 +1,80 @@
+package validate
+
+import (
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestArray(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+	}{
+		{"Max Array", field.Array(field.String()).AddOptions(MaxItems(2)), []interface{}{"a", "b"}, false},
+		{"Max Array (invalid)", field.Array(field.String()).AddOptions(MaxItems(2)), []interface{}{"a", "b", "c"}, true},
+		{"Max Array (negative max number)", field.Array(field.String()).AddOptions(MaxItems(-2)), []interface{}{"a", "b", "c"}, false},
+		{"Max Array (zero max number)", field.Array(field.String()).AddOptions(MaxItems(0)), []interface{}{"a", "b", "c"}, false},
+
+		{"Min Array", field.Array(field.String()).AddOptions(MinItems(2)), []interface{}{"a", "b"}, false},
+		{"Min Array (invalid)", field.Array(field.String()).AddOptions(MinItems(2)), []interface{}{"a"}, true},
+		{"Min Array (negative max number)", field.Array(field.String()).AddOptions(MinItems(-2)), []interface{}{"a", "b", "c"}, false},
+		{"Min Array (zero max number)", field.Array(field.String()).AddOptions(MinItems(0)), []interface{}{"a", "b", "c"}, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := field.Decode(nil, tt.field, tt.data)
+			require.NoError(t, err)
+			err = Validate(nil, tt.field, got)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+		})
+	}
+}
+
+func TestArrayValidate(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+		error   string
+	}{
+		{"Nil Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), nil, false, ""},
+		{"Nil Min Items", field.Array(field.String()).AddOptions(MinItems(1)), nil, false, ""},
+		{"Array Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), [1]interface{}{1}, true, "validation error: incorrect type: \"array\", expected \"[]interface{}\""},
+		{"Array Min Items", field.Array(field.String()).AddOptions(MinItems(1)), [1]interface{}{1}, true, "validation error: incorrect type: \"array\", expected \"[]interface{}\""},
+		{"Slice Max Items", field.Array(field.String()).AddOptions(MaxItems(0)), []interface{}{}, false, ""},
+		{"Slice Min Items", field.Array(field.String()).AddOptions(MinItems(0)), []interface{}{}, false, ""},
+		{"Bool Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), true, true, "validation error: incorrect type: \"bool\", expected \"array\""},
+		{"Bool Min Items", field.Array(field.String()).AddOptions(MinItems(1)), true, true, "validation error: incorrect type: \"bool\", expected \"array\""},
+		{"Int Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), 1, true, "validation error: incorrect type: \"int\", expected \"array\""},
+		{"Int Min Items", field.Array(field.String()).AddOptions(MinItems(1)), 1, true, "validation error: incorrect type: \"int\", expected \"array\""},
+		{"Float Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), 1.0, true, "validation error: incorrect type: \"float64\", expected \"array\""},
+		{"Float Min Items", field.Array(field.String()).AddOptions(MinItems(1)), 1.0, true, "validation error: incorrect type: \"float64\", expected \"array\""},
+		{"String Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), "1", true, "validation error: incorrect type: \"string\", expected \"array\""},
+		{"String Min Items", field.Array(field.String()).AddOptions(MinItems(1)), "1", true, "validation error: incorrect type: \"string\", expected \"array\""},
+		{"Map Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), map[int]string{1: "1"}, true, "validation error: incorrect type: \"map\", expected \"array\""},
+		{"Map Min Items", field.Array(field.String()).AddOptions(MinItems(1)), map[int]string{1: "1"}, true, "validation error: incorrect type: \"map\", expected \"array\""},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := Validate(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.error)
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/validate/enum.go b/pkg/schema/validate/enum.go
new file mode 100644
index 0000000000000000000000000000000000000000..76d2ecc698792346e1f9dd60940e47d050593b96
--- /dev/null
+++ b/pkg/schema/validate/enum.go
@@ -0,0 +1,68 @@
+package validate
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/pkg/errors"
+)
+
+type EnumUI struct {
+	Color string `json:"color,omitempty"`
+	Icon  string `json:"icon,omitempty"`
+	Spin  bool   `json:"spin,omitempty"`
+	Blink bool   `json:"blink,omitempty"`
+}
+
+type EnumOpt struct {
+	Name  string      `json:"name"`
+	Value interface{} `json:"value"`
+	UI    *EnumUI     `json:"ui,omitempty"`
+}
+
+func (o EnumOpt) String() string {
+	return fmt.Sprintf("%s", o.Value)
+}
+
+type enum []EnumOpt
+
+func Enum(opts ...EnumOpt) Validator {
+	var e enum
+	for _, i := range opts {
+		e = append(e, i)
+	}
+	return &e
+}
+
+func (t enum) Validate(ctx context.Context, f *field.Field, value interface{}) error {
+	// Значение может отсутствовать, что не является ошибкой
+	if value == nil {
+		return nil
+	}
+
+	if f.AdditionalValues {
+		return nil
+	}
+
+	for _, i := range t {
+		enumValue, err := field.Decode(ctx, f, i.Value)
+		if err != nil {
+			return fmt.Errorf("error decode option value: %w", err)
+		}
+		if value == enumValue {
+			return nil
+		}
+	}
+
+	return errors.Errorf("value required to be one of %s", t)
+}
+
+func (t enum) ValidateOption() error {
+	for _, i := range t {
+		if i.Name == "" {
+			return errors.Errorf("enum name is required")
+		}
+	}
+	return nil
+}
diff --git a/pkg/schema/validate/enum_test.go b/pkg/schema/validate/enum_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a473dc015f80e92a2b0845228b02023f3f92b63a
--- /dev/null
+++ b/pkg/schema/validate/enum_test.go
@@ -0,0 +1,69 @@
+package validate
+
+import (
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/stretchr/testify/require"
+)
+
+func TestEnum(t *testing.T) {
+	stringEnum := Enum(
+		EnumOpt{
+			Name:  "N 1",
+			Value: "n1",
+		},
+		EnumOpt{
+			Name:  "N 2",
+			Value: "n2",
+		},
+	)
+	intEnum := Enum(
+		EnumOpt{
+			Name:  "N 1",
+			Value: 1,
+		},
+		EnumOpt{
+			Name:  "N 2",
+			Value: 2,
+		},
+	)
+	floatEnum := Enum(
+		EnumOpt{
+			Name:  "N 1",
+			Value: 1.1,
+		},
+		EnumOpt{
+			Name:  "N 2",
+			Value: int(2),
+		},
+	)
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+	}{
+		{"Correct string", field.String(stringEnum), "n1", false},
+		{"Correct int", field.Number(field.NumberFormatInt, intEnum), 1, false},
+		{"Correct float", field.Number(field.NumberFormatFloat, floatEnum), 1.1, false},
+		{"Correct float", field.Number(field.NumberFormatFloat, floatEnum), int(2), false},
+		{"Incorrect string", field.String(stringEnum), "n3", true},
+		{"Incorrect int", field.Number(field.NumberFormatInt, intEnum), 3, true},
+		{"Incorrect float", field.Number(field.NumberFormatFloat, floatEnum), 1.0, true},
+		{"Incorrect enum value", field.String(intEnum), "n1", true},
+		{"Incorrect enum value", field.String(stringEnum).SetAdditionalValues(), "non enum value", false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := field.Decode(nil, tt.field, tt.data)
+			require.NoError(t, err)
+			err = Validate(nil, tt.field, got)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+		})
+	}
+}
diff --git a/pkg/schema/validate/number.go b/pkg/schema/validate/number.go
new file mode 100644
index 0000000000000000000000000000000000000000..81ab150b21d0af3a5e6cc91dac664c3a479a9f11
--- /dev/null
+++ b/pkg/schema/validate/number.go
@@ -0,0 +1,136 @@
+package validate
+
+import (
+	"context"
+	"fmt"
+	"math"
+	"reflect"
+	"strconv"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+type max float64
+
+func Max(m float64) Validator {
+	v := max(m)
+	return &v
+}
+
+func (t max) Validate(_ context.Context, field *field.Field, value interface{}) error {
+	var num float64
+	switch v := value.(type) {
+	case int:
+		num = float64(v)
+	case int8:
+		num = float64(v)
+	case int32:
+		num = float64(v)
+	case int64:
+		num = float64(v)
+	case uint:
+		num = float64(v)
+	case uint8:
+		num = float64(v)
+	case uint32:
+		num = float64(v)
+	case uint64:
+		num = float64(v)
+	case float32:
+		num = float64(v)
+	case float64:
+		num = v
+	case nil:
+		return nil
+	default:
+		return fmt.Errorf("incorrect type: \"%s\", expected \"number\"", reflect.ValueOf(v).Kind())
+	}
+	if num > float64(t) {
+		return fmt.Errorf("maximum number is %s, got %s", strconv.FormatFloat(float64(t), 'f', -1, 64), strconv.FormatFloat(float64(num), 'f', -1, 64))
+	}
+	return nil
+
+}
+
+type min float64
+
+func Min(m float64) Validator {
+	v := min(m)
+	return &v
+}
+
+func (t min) Validate(_ context.Context, fld *field.Field, value interface{}) error {
+	var num float64
+	switch v := value.(type) {
+	case int:
+		num = float64(v)
+	case int8:
+		num = float64(v)
+	case int32:
+		num = float64(v)
+	case int64:
+		num = float64(v)
+	case uint:
+		num = float64(v)
+	case uint8:
+		num = float64(v)
+	case uint32:
+		num = float64(v)
+	case uint64:
+		num = float64(v)
+	case float32:
+		num = float64(v)
+	case float64:
+		num = v
+	case nil:
+		return nil
+	default:
+		return fmt.Errorf("incorrect type: \"%s\", expected \"number\"", reflect.ValueOf(v).Kind())
+	}
+	if num < float64(t) {
+		return fmt.Errorf("minimum number is %s, got %s", strconv.FormatFloat(float64(t), 'f', -1, 64), strconv.FormatFloat(float64(num), 'f', -1, 64))
+	}
+	return nil
+}
+
+type multipleOf float64
+
+func MultipleOf(d float64) Validator {
+	v := multipleOf(d)
+	return &v
+}
+
+func (t multipleOf) Validate(_ context.Context, field *field.Field, value interface{}) error {
+	var num float64
+	switch v := value.(type) {
+	case int:
+		num = float64(v)
+	case int8:
+		num = float64(v)
+	case int32:
+		num = float64(v)
+	case int64:
+		num = float64(v)
+	case uint:
+		num = float64(v)
+	case uint8:
+		num = float64(v)
+	case uint32:
+		num = float64(v)
+	case uint64:
+		num = float64(v)
+	case float32:
+		num = float64(v)
+	case float64:
+		num = v
+	case nil:
+		return nil
+	default:
+		return fmt.Errorf("incorrect type: \"%s\", expected \"number\"", reflect.ValueOf(v).Kind())
+	}
+	if math.Mod(num, float64(t)) != 0 {
+		return fmt.Errorf("number must be a multiple of %f", t)
+
+	}
+	return nil
+}
diff --git a/pkg/schema/validate/number_test.go b/pkg/schema/validate/number_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..edbd218cf3b19ecd5b412e5a7e74015a8dfaecb5
--- /dev/null
+++ b/pkg/schema/validate/number_test.go
@@ -0,0 +1,99 @@
+package validate
+
+import (
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNumber(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+	}{
+		{"Max Int", field.Number("int").AddOptions(Max(5.0)), 6, true},
+		{"Min Int", field.Number("float").AddOptions(Min(10)), 9.0, true},
+
+		{"Max Float", field.Number("float").AddOptions(Max(5.5)), 5.6, true},
+		{"Min Float", field.Number("float").AddOptions(Min(10.1)), 9.9, true},
+
+		{"MultipleOf Int", field.Number("float").AddOptions(MultipleOf(5)), 1.0, true},
+		{"MultipleOf Int", field.Number("float").AddOptions(MultipleOf(5)), 10.0, false},
+
+		{"MultipleOf Float", field.Number("float").AddOptions(MultipleOf(0.01)), 0.2, true},
+		{"MultipleOf Float", field.Number("float").AddOptions(MultipleOf(0.05)), 0.1, false},
+
+		{"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(5), Min(2), MultipleOf(5)), 6, true},
+		{"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(5), Min(2), MultipleOf(2)), 4, false},
+		{"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(5), Min(1), MultipleOf(2)), -2, true},
+		{"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(5), Min(-1), MultipleOf(2)), 0, false},
+		{"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(10), Min(7), MultipleOf(5)), 6, true},
+		{"MinMaxMultipleOf Float", field.Number("int").AddOptions(Max(5.5), Min(2.3), MultipleOf(5)), 6, true},
+		{"MinMaxMultipleOf Float", field.Number("float").AddOptions(Max(10.1), Min(7.7), MultipleOf(0.1)), 6.1, true},
+
+		{"Enum miss", field.Number("int").AddOptions(Enum(EnumOpt{Name: "N 1", Value: 1}, EnumOpt{Name: "N 2", Value: 2})), 3, true},
+		{"Enum match", field.Number("int").AddOptions(Enum(EnumOpt{Name: "N 1", Value: 1}, EnumOpt{Name: "N 2", Value: 2})), 2, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := field.Decode(nil, tt.field, tt.data)
+			require.NoError(t, err)
+			err = Validate(nil, tt.field, got)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+		})
+	}
+}
+
+func TestNumberValidate(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+		error   string
+	}{
+		{"Nil Max", field.Number("int").AddOptions(Max(1.0)), nil, false, ""},
+		{"Nil Min", field.Number("int").AddOptions(Min(1.0)), nil, false, ""},
+		{"Nil MultipleOf", field.Number("int").AddOptions(MultipleOf(1)), nil, false, ""},
+		{"Int Max", field.Number("int").AddOptions(Max(1.0)), 1, false, ""},
+		{"Int Min", field.Number("int").AddOptions(Min(1.0)), 1, false, ""},
+		{"Int MultipleOf", field.Number("int").AddOptions(Min(1.0)), 1, false, ""},
+		{"Flоat Max", field.Number("float").AddOptions(Max(1.0)), 1.0, false, ""},
+		{"Flоat Min", field.Number("float").AddOptions(Min(1.0)), 1.0, false, ""},
+		{"Flоat MultipleOf", field.Number("float").AddOptions(Min(1.0)), 1.0, false, ""},
+		{"Bool Max Num", field.Number("int").AddOptions(Max(1.0)), true, true, "validation error: incorrect type: \"bool\", expected \"number\""},
+		{"Bool Min Num", field.Number("int").AddOptions(Min(1.0)), true, true, "validation error: incorrect type: \"bool\", expected \"number\""},
+		{"Bool MultipleOf Num", field.Number("int").AddOptions(MultipleOf(1)), true, true, "validation error: incorrect type: \"bool\", expected \"number\""},
+		{"String Max Num", field.Number("int").AddOptions(Max(1.0)), "1", true, "validation error: incorrect type: \"string\", expected \"number\""},
+		{"String Min Num", field.Number("int").AddOptions(Min(1.0)), "1", true, "validation error: incorrect type: \"string\", expected \"number\""},
+		{"String MultipleOf Num", field.Number("int").AddOptions(MultipleOf(1)), "1", true, "validation error: incorrect type: \"string\", expected \"number\""},
+		{"Array Max Num", field.Number("int").AddOptions(Max(1.0)), [1]int{1}, true, "validation error: incorrect type: \"array\", expected \"number\""},
+		{"Array Min Num", field.Number("int").AddOptions(Min(1.0)), [1]int{1}, true, "validation error: incorrect type: \"array\", expected \"number\""},
+		{"Slice Max Num", field.Number("int").AddOptions(Max(1.0)), []int{1}, true, "validation error: incorrect type: \"slice\", expected \"number\""},
+		{"Slice Min Num", field.Number("int").AddOptions(Min(1.0)), []int{1}, true, "validation error: incorrect type: \"slice\", expected \"number\""},
+		{"Slice MultipleOf Num", field.Number("int").AddOptions(MultipleOf(1)), []int{}, true, "validation error: incorrect type: \"slice\", expected \"number\""},
+		{"Map Max Num", field.Number("int").AddOptions(Max(1.0)), map[int]int{}, true, "validation error: incorrect type: \"map\", expected \"number\""},
+		{"Map Min Num", field.Number("int").AddOptions(Min(1.0)), map[int]int{}, true, "validation error: incorrect type: \"map\", expected \"number\""},
+		{"Map MultipleOf Num", field.Number("int").AddOptions(MultipleOf(1)), map[int]int{}, true, "validation error: incorrect type: \"map\", expected \"number\""},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := Validate(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.error)
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/validate/readonly.go b/pkg/schema/validate/readonly.go
new file mode 100644
index 0000000000000000000000000000000000000000..8738d747dd1d86fad7203f20b9df83cb5d2334a4
--- /dev/null
+++ b/pkg/schema/validate/readonly.go
@@ -0,0 +1,30 @@
+package validate
+
+import (
+	"context"
+	"errors"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+type readonly bool
+
+func ReadOnly() Validator {
+	r := readonly(true)
+	return &r
+}
+
+func (r readonly) Validate(_ context.Context, f *field.Field, value interface{}) error {
+	if value != nil {
+		return errors.New("value is readonly")
+	}
+	return nil
+
+}
+
+func (r readonly) ValidateOption() error {
+	if !r {
+		return field.ErrSkipOption
+	}
+	return nil
+}
diff --git a/pkg/schema/validate/readonly_test.go b/pkg/schema/validate/readonly_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ee8ca9ba89a16a02375556d365884bb35d378f83
--- /dev/null
+++ b/pkg/schema/validate/readonly_test.go
@@ -0,0 +1,32 @@
+package validate
+
+import (
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/stretchr/testify/require"
+)
+
+func TestReadonly(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+	}{
+		{"ReadOnly", field.String(ReadOnly()), nil, false},
+		{"ReadOnly error", field.String(ReadOnly()), "1234567", true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := field.Decode(nil, tt.field, tt.data)
+			require.NoError(t, err)
+			err = Validate(nil, tt.field, got)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+		})
+	}
+}
diff --git a/pkg/schema/validate/required.go b/pkg/schema/validate/required.go
new file mode 100644
index 0000000000000000000000000000000000000000..96591e6eaf4134f07523ab87f29e932b8fcee60e
--- /dev/null
+++ b/pkg/schema/validate/required.go
@@ -0,0 +1,48 @@
+package validate
+
+import (
+	"context"
+	"errors"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+type required bool
+
+func Required() Validator {
+	r := required(true)
+	return &r
+}
+
+func (r required) Validate(_ context.Context, f *field.Field, value interface{}) error {
+
+	if value != nil {
+		t, ok := f.GetType().(interface{ IsEmpty(interface{}) bool })
+		if ok && !t.IsEmpty(value) {
+			return nil
+		}
+
+		/*
+			tt, ok := f.GetType().(interface {
+					IsEmpty(interface{}) (bool, error)
+				})
+				empty, err := tt.IsEmpty(value)
+				if err != nil {
+					return err
+				}
+				if ok && !empty {
+					return nil
+				}
+		*/
+	}
+
+	return errors.New("value is required")
+
+}
+
+func (r required) ValidateOption() error {
+	if !r {
+		return field.ErrSkipOption
+	}
+	return nil
+}
diff --git a/pkg/schema/validate/required_test.go b/pkg/schema/validate/required_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..41a03ab50d6fab906e7f78229b560e4298475dde
--- /dev/null
+++ b/pkg/schema/validate/required_test.go
@@ -0,0 +1,53 @@
+package validate
+
+import (
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRequired(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+	}{
+		{"Array no value", field.Array(field.String(), Required()), nil, true},
+		{"Array empty", field.Array(field.String(), Required()), []interface{}{}, true},
+		{"Array not empty", field.Array(field.String(), Required()), []interface{}{"a", "b"}, false},
+		{"Boolean no value", field.Bool(Required()), nil, true},
+		{"Boolean", field.Bool(Required()), false, false},
+		{"Location: no value", field.Location(Required()), nil, true},
+		//{"Location: empty", field.Location(Required()), map[string]interface{}{}, true}, // не имеет смысла, при Decode вернется ошибка если объект пустой
+		{"Location: not empty", field.Location(Required()), &field.GeoObject{Address: "addr"}, false},
+		{"Number (int) no value", field.Number(field.NumberFormatInt, Required()), nil, true},
+		{"Number (int) empty", field.Number(field.NumberFormatInt, Required()), 0, false},
+		{"Number (int) not empty", field.Number(field.NumberFormatInt, Required()), 42, false},
+		{"Number (float) no value", field.Number(field.NumberFormatFloat, Required()), nil, true},
+		{"Number (float) empty", field.Number(field.NumberFormatFloat, Required()), 0.0, false},
+		{"Number (float) not empty", field.Number(field.NumberFormatFloat, Required()), 42.2, false},
+		{"Object no value", field.Object("a", field.String(), "b", field.Bool(), Required()), nil, true},
+		{"Object no value", field.Object("a", field.String(), "b", field.Bool(), Required()), map[string]interface{}{}, true},
+		{"Object not empty", field.Object("a", field.String(), "b", field.Bool(), Required()), map[string]interface{}{"b": true}, false},
+		{"String no value", field.String(Required()), nil, true},
+		{"String empty", field.String(Required()), "", true},
+		{"String not empty", field.String(Required()), "1234567", false},
+		{"Time no value", field.Time(Required()), nil, true},
+		{"Time not empty", field.Time(Required()), time.Now().Format(time.RFC3339), false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := field.Decode(nil, tt.field, tt.data)
+			require.NoError(t, err)
+			err = Validate(nil, tt.field, got)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+		})
+	}
+}
diff --git a/pkg/schema/validate/string.go b/pkg/schema/validate/string.go
new file mode 100644
index 0000000000000000000000000000000000000000..f05ca540034e3cfab7e57c1aa324bcb343b97431
--- /dev/null
+++ b/pkg/schema/validate/string.go
@@ -0,0 +1,71 @@
+package validate
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"unicode/utf8"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/pkg/errors"
+)
+
+type maxLength int
+
+func MaxLength(max int) Validator {
+	v := maxLength(max)
+	return &v
+}
+
+func (t maxLength) Validate(_ context.Context, field *field.Field, value interface{}) error {
+	if s, ok := value.(string); ok {
+		n := utf8.RuneCountInString(s)
+		if t > 0 && n > int(t) {
+			return fmt.Errorf("maximum string length is %d, got \"%s\" (length=%d)", t, s, len(s))
+		}
+		return nil
+	}
+	return fmt.Errorf("incorrect type: \"%s\", expected \"string\"", reflect.ValueOf(value).Kind())
+}
+
+type minLength int
+
+func MinLength(max int) Validator {
+	v := minLength(max)
+	return &v
+}
+
+func (t minLength) Validate(_ context.Context, field *field.Field, value interface{}) error {
+	if s, ok := value.(string); ok {
+		n := utf8.RuneCountInString(s)
+		if n < int(t) {
+			return fmt.Errorf("minimum string length is %d, got \"%s\" (length=%d)", t, s, len(s))
+		}
+		return nil
+	}
+	return fmt.Errorf("incorrect type: \"%s\", expected \"string\"", reflect.ValueOf(value).Kind().String())
+}
+
+type schema bool
+
+func Schema() Validator {
+	v := schema(true)
+	return &v
+}
+
+func (t schema) Validate(_ context.Context, _ *field.Field, value interface{}) error {
+	if value == nil {
+		return nil
+	}
+	if s, ok := value.(string); ok {
+		if s == "" {
+			return nil
+		}
+		sch := field.Object()
+		if err := sch.UnmarshalJSON([]byte(s)); err != nil {
+			return errors.New("value is not valid schema")
+		}
+		return nil
+	}
+	return errors.Errorf("incorrect type: \"%s\", expected string", reflect.ValueOf(value).Kind())
+}
diff --git a/pkg/schema/validate/string_test.go b/pkg/schema/validate/string_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b67e8c8d4f5056c61d5483a536701777d716bead
--- /dev/null
+++ b/pkg/schema/validate/string_test.go
@@ -0,0 +1,160 @@
+package validate
+
+import (
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestString(t *testing.T) {
+	invalidOptionsSchema := `{
+	"type": "object",
+	"params": {
+		"fields": {
+			"required": {
+				"options": {
+					"err": true
+				},
+				"type": "string"
+			}
+		}
+	}
+}`
+	requiredOptionsSchema := `{
+	"type": "object",
+	"params": {
+		"fields": {
+			"enum": {
+				"options": {
+					"enum": [{
+							"value": "one"
+						},
+						{
+							"value": "two"
+						}
+					]
+				},
+				"type": "string"
+			}
+		}
+	}
+}`
+
+	validSchema := `{
+	"type": "object",
+	"params": {
+		"fields": {
+			"required": {
+				"options": {
+					"required": true
+				},
+				"type": "string"
+			},
+			"readonly": {
+				"options": {
+					"readonly": true
+				},
+				"type": "string"
+			},
+			"enum": {
+				"options": {
+					"enum": [{
+							"name": "One",
+							"value": "one"
+						},
+						{
+							"name": "Two",
+							"value": "two"
+						}
+					]
+				},
+				"type": "string"
+			}
+		}
+	}
+}`
+
+	unknownFieldSchema := `{
+	"type": "object",
+	"params": {
+		"fields": {
+			"string": {}
+		}
+	}
+}`
+
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+	}{
+		{"Length Max", field.String().AddOptions(MaxLength(5)), "1234567", true},
+		{"Length Min", field.String().AddOptions(MinLength(10)), "1234", true},
+		{"Length MinMax", field.String().AddOptions(MaxLength(6), MinLength(2)), "1234567", true},
+		{"Length MinMax", field.String().AddOptions(MaxLength(10), MinLength(7)), "123456", true},
+		{"Enum miss", field.String().AddOptions(Enum(EnumOpt{Name: "N 1", Value: "n1"}, EnumOpt{Name: "N 2", Value: "n2"})), "n3", true},
+		{"Enum match", field.String().AddOptions(Enum(EnumOpt{Name: "N 1", Value: "n1"}, EnumOpt{Name: "N 2", Value: "n2"})), "n2", false},
+		{"Invalid Schema Options", field.String().AddOptions(Schema()), invalidOptionsSchema, true},
+		{"Required Schema Options", field.String().AddOptions(Schema()), requiredOptionsSchema, true},
+		{"Valid Schema", field.String().AddOptions(Schema()), validSchema, false},
+		{"Invalid Schema#1", field.String().AddOptions(Schema()), "test", true},
+		{"Unknown Field", field.String().AddOptions(Schema()), unknownFieldSchema, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := field.Decode(nil, tt.field, tt.data)
+			require.NoError(t, err)
+			err = Validate(nil, tt.field, got)
+			if tt.wantErr {
+				require.Error(t, err)
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestStringValidate(t *testing.T) {
+	tests := []struct {
+		name    string
+		field   *field.Field
+		data    interface{}
+		wantErr bool
+		error   string
+	}{
+		{"String Length Max", field.String().AddOptions(MaxLength(1)), "1", false, ""},
+		{"String Length Min", field.String().AddOptions(MinLength(1)), "1", false, ""},
+		{"Nil Length Max", field.String().AddOptions(MaxLength(1)), nil, true, "validation error: incorrect type: \"invalid\", expected \"string\""},
+		{"Nil Length Min", field.String().AddOptions(MinLength(1)), nil, true, "validation error: incorrect type: \"invalid\", expected \"string\""},
+		{"Int Length Max", field.String().AddOptions(MaxLength(1)), 1, true, "validation error: incorrect type: \"int\", expected \"string\""},
+		{"Int Length Min", field.String().AddOptions(MinLength(1)), 1, true, "validation error: incorrect type: \"int\", expected \"string\""},
+		{"Float Length Max", field.String().AddOptions(MaxLength(1)), 1.0, true, "validation error: incorrect type: \"float64\", expected \"string\""},
+		{"Float Length Min", field.String().AddOptions(MinLength(1)), 1.0, true, "validation error: incorrect type: \"float64\", expected \"string\""},
+		{"Bool Length Max", field.String().AddOptions(MaxLength(1)), true, true, "validation error: incorrect type: \"bool\", expected \"string\""},
+		{"Bool Length Min", field.String().AddOptions(MinLength(1)), true, true, "validation error: incorrect type: \"bool\", expected \"string\""},
+		{"Array Length Max", field.String().AddOptions(MaxLength(1)), [1]string{""}, true, "validation error: incorrect type: \"array\", expected \"string\""},
+		{"Array Length Min", field.String().AddOptions(MinLength(1)), [1]string{""}, true, "validation error: incorrect type: \"array\", expected \"string\""},
+		{"Slice Length Max", field.String().AddOptions(MaxLength(1)), []string{""}, true, "validation error: incorrect type: \"slice\", expected \"string\""},
+		{"Slice Length Min", field.String().AddOptions(MinLength(1)), []string{""}, true, "validation error: incorrect type: \"slice\", expected \"string\""},
+		{"Map Length Max", field.String().AddOptions(MaxLength(1)), map[string]string{"": ""}, true, "validation error: incorrect type: \"map\", expected \"string\""},
+		{"Map Length Min", field.String().AddOptions(MinLength(1)), map[string]string{"": ""}, true, "validation error: incorrect type: \"map\", expected \"string\""},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := Validate(nil, tt.field, tt.data)
+			if tt.wantErr {
+				require.Error(t, err)
+				assert.EqualError(t, err, tt.error)
+			}
+			if !tt.wantErr {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/schema/validate/validate.go b/pkg/schema/validate/validate.go
new file mode 100644
index 0000000000000000000000000000000000000000..65ac4c400d19055deae67ddfcc5678775d0e3376
--- /dev/null
+++ b/pkg/schema/validate/validate.go
@@ -0,0 +1,99 @@
+package validate
+
+import (
+	"context"
+	"sort"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/expr"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+const ValidatorPriority = 2000
+
+type Validator interface {
+	Validate(ctx context.Context, f *field.Field, v interface{}) error
+}
+
+type Validators []Validator
+
+func (l Validators) Len() int { return len(l) }
+func (l Validators) Less(i, j int) bool {
+	pi, pj := ValidatorPriority, ValidatorPriority
+	if o, ok := l[i].(field.PriorityOption); ok {
+		pi = o.GetPriority()
+	}
+	if o, ok := l[j].(field.PriorityOption); ok {
+		pj = o.GetPriority()
+	}
+	if pi == pj {
+		return field.GetOptionName(l[i]) < field.GetOptionName(l[j])
+	}
+	return pi < pj
+}
+func (l Validators) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
+
+func getValidators(f *field.Field) Validators {
+	var vs Validators
+	for _, o := range f.Options {
+		if v, ok := o.(Validator); ok {
+			vs = append(vs, v)
+		}
+	}
+	sort.Sort(vs)
+	return vs
+}
+
+func validateOptions(ctx context.Context, f *field.Field, v interface{}) error {
+	var err error
+	validators := getValidators(f)
+	for _, i := range validators {
+		err = i.Validate(ctx, f, v)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func Validate(ctx context.Context, w field.Walker, v interface{}) error {
+	if m, ok := v.(map[string]interface{}); ok {
+		ctx = expr.WithEnv(ctx, m)
+	}
+
+	_, _, err := w.Walk(ctx, v, func(ctx context.Context, fld *field.Field, v interface{}) (res field.WalkFuncResult, err error) {
+		enabled, _ := fld.IsEnabled(ctx)
+
+		res.Value = v // Значение не меняется
+
+		if !enabled {
+			res.Stop = true
+			return
+		}
+
+		if err = validateOptions(ctx, fld, v); err != nil {
+			return
+		}
+
+		if validator, ok := fld.GetType().(Validator); ok {
+			err = validator.Validate(ctx, fld, v)
+		}
+
+		return
+	})
+
+	return errors.Wrap(err, "validation error")
+}
+
+func init() {
+	field.RegisterOption(minLength(0))
+	field.RegisterOption(maxLength(0))
+	field.RegisterOption(min(0))
+	field.RegisterOption(max(0))
+	field.RegisterOption(multipleOf(1))
+	field.RegisterOption(enum{})
+	field.RegisterOption(readonly(true))
+	field.RegisterOption(required(true))
+	field.RegisterOption(maxItems(0))
+	field.RegisterOption(schema(true))
+}
diff --git a/pkg/schema/walk/fn.go b/pkg/schema/walk/fn.go
new file mode 100644
index 0000000000000000000000000000000000000000..c3eaf35c569c18ba8ead7c7ff48c08d03f2a9d90
--- /dev/null
+++ b/pkg/schema/walk/fn.go
@@ -0,0 +1,11 @@
+package walk
+
+func GenericMerge(c *WalkContext) (err error) {
+	return
+}
+
+func KeepSrc(c *WalkContext) (err error) {
+	c.Dst = c.Src
+	c.Changed = true
+	return
+}
diff --git a/pkg/schema/walk/walk.go b/pkg/schema/walk/walk.go
new file mode 100644
index 0000000000000000000000000000000000000000..4757742f82efcaffd5faba47eb076ed727783c75
--- /dev/null
+++ b/pkg/schema/walk/walk.go
@@ -0,0 +1,182 @@
+package walk
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+// DataFunc тип для функции обработки данных
+type DataFunc func(c *WalkContext) error
+
+// FieldConfig описывает какие действия с полем нужно предпринять при обходе данных
+type FieldConfig struct {
+	Fn DataFunc // Пользовательская функция обработки данных поля
+}
+
+// WalkConfig настройки обхода данных
+type WalkConfig struct {
+	// Настройки для полей, в качестве ключа указывается абсолютный путь поля
+	// Например: "a.b.c.1" (числа для slice)
+	Fields map[string]FieldConfig
+}
+
+// Walker позволяет выполнять обход данных для соответствующей схемы
+type Walker struct {
+	schema    *schema.Schema
+	config    *WalkConfig
+	DefaultFn DataFunc // Функция обработки данных применяемая по умолчанию
+}
+
+// NewWalker создает экземпляр
+func NewWalker(schema *schema.Schema, config *WalkConfig) *Walker {
+	return &Walker{
+		schema:    schema,
+		config:    config,
+		DefaultFn: GenericMerge,
+	}
+}
+
+// WalkContext контекст обхода данных
+type WalkContext struct {
+	Ctx     context.Context
+	Path    string       // Путь к родительским данным
+	Key     interface{}  // Ключ или индекс текущих данных
+	Field   *field.Field // Поля схемы соответсвующее текущим данным
+	Dst     interface{}  // Данные приемника
+	Src     interface{}  // Данные источника
+	Changed bool         // Флаг показывающий, что данные приемника изменились
+}
+
+// GetPath возвращает путь соответсвующий текущему контексту
+func (w WalkContext) GetPath(keys ...interface{}) string {
+	p := make([]string, 0, 10)
+
+	if w.Path != "" {
+		p = append(p, w.Path)
+	}
+
+	if w.Key != nil {
+		p = append(p, fmt.Sprintf("%v", w.Key))
+	}
+
+	for _, k := range keys {
+		p = append(p, fmt.Sprintf("%v", k))
+	}
+
+	return strings.Join(p, ".")
+}
+
+// Clone создает копию контекста
+func (w WalkContext) Clone() *WalkContext {
+	return &w
+}
+
+// DataWalk выполняет обход данных и возвращает измененные данные
+func (m *Walker) DataWalk(ctx context.Context, dst, src interface{}) (res interface{}, changed bool, err error) {
+	wc := &WalkContext{
+		Ctx:   ctx,
+		Field: &m.schema.Field,
+		Dst:   dst,
+		Src:   src,
+	}
+
+	err = m.datawalk(wc)
+
+	return wc.Dst, wc.Changed, err
+}
+
+func (m *Walker) datawalk(w *WalkContext) (err error) {
+	path := w.GetPath()
+	fn := m.DefaultFn
+
+	fieldCfg, _ := m.config.Fields[path]
+
+	if fieldCfg.Fn != nil {
+		fn = fieldCfg.Fn
+	}
+
+	if err = fn(w); err != nil {
+		return
+	}
+
+	switch p := w.Field.Params.(type) {
+
+	case *field.ObjectParameters:
+		d, _ := w.Dst.(map[string]interface{})
+		s, _ := w.Src.(map[string]interface{})
+		res := make(map[string]interface{})
+
+		keys := make(map[string]struct{})
+		for k := range d {
+			keys[k] = struct{}{}
+		}
+		for k := range s {
+			keys[k] = struct{}{}
+		}
+
+		fields := p.GetFields(true)
+
+		for k := range keys {
+			f, ok := fields[k]
+			if !ok {
+				continue
+			}
+
+			wc := WalkContext{
+				Ctx:   w.Ctx,
+				Path:  w.GetPath(),
+				Key:   k,
+				Field: f,
+				Dst:   d[k],
+				Src:   s[k],
+			}
+
+			if err = m.datawalk(&wc); err != nil {
+				return
+			}
+
+			if wc.Dst != nil {
+				res[k] = wc.Dst
+			}
+
+			if wc.Changed {
+				w.Changed = true
+			}
+		}
+		if len(res) > 0 {
+			w.Dst = res
+		}
+
+	case *field.ArrayParameters:
+		d, _ := w.Dst.([]interface{})
+		s, _ := w.Src.([]interface{})
+		for i, v := range d {
+			var src_v interface{}
+			if i < len(s) {
+				src_v = s[i]
+			}
+			wc := WalkContext{
+				Ctx:   w.Ctx,
+				Path:  w.GetPath(),
+				Key:   i,
+				Field: p.Item,
+				Dst:   v,
+				Src:   src_v,
+			}
+			if err = m.datawalk(&wc); err != nil {
+				return
+			}
+			if wc.Changed {
+				d[i] = wc.Dst
+				w.Changed = true
+			}
+		}
+
+	}
+
+	return
+}
diff --git a/pkg/schema/walk/walk_test.go b/pkg/schema/walk/walk_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e20adb462735e83c92c8b04f5c159e336e49f2b0
--- /dev/null
+++ b/pkg/schema/walk/walk_test.go
@@ -0,0 +1,115 @@
+package walk
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestWalker_DataWalk(t *testing.T) {
+	s := schema.New(
+		"a", field.String(),
+		"b", field.String(),
+		"obj1", field.Object(
+			"a", field.String(),
+			"b", field.String(),
+			"obj2", field.Object(
+				"a", field.String(),
+				"b", field.String(),
+			),
+			"obj3", field.Object(
+				"e", field.String(),
+			),
+		),
+		"slice", field.Array(field.String()),
+		"inline_outer_str", field.Object(
+			true,
+			"inline_str_1", field.String(),
+			"inline_obj", field.Object(
+				true,
+				"inline_str_2", field.String(),
+			),
+		),
+	)
+
+	tests := []struct {
+		name        string
+		config      *WalkConfig
+		src         map[string]interface{}
+		dst         map[string]interface{}
+		res         map[string]interface{}
+		wantChanged bool
+		wantErr     bool
+	}{
+		{"generic",
+			&WalkConfig{
+				Fields: map[string]FieldConfig{
+					"obj1.a":       {Fn: KeepSrc},
+					"slice.1":      {Fn: KeepSrc},
+					"inline_str_1": {Fn: KeepSrc},
+					"inline_str_2": {Fn: KeepSrc},
+				},
+			},
+			map[string]interface{}{
+				"a": "src_a",
+				"b": "src_b",
+				"obj1": map[string]interface{}{
+					"a": "src_obj1_a",
+					"b": "src_obj1_b",
+					"obj2": map[string]interface{}{
+						"a": "dst_obj1_obj2_a",
+					},
+					"obj3": map[string]interface{}{
+						"e": "dst_obj1_obj3_e",
+					},
+				},
+				"inline_str_1": "src_inline_1",
+				"inline_str_2": "src_inline_2",
+				"slice":        []interface{}{"src_s1", "src_s2"},
+			},
+			map[string]interface{}{
+				"a":                 "dst_a",
+				"field_not_extists": "remove",
+				"obj1": map[string]interface{}{
+					"a": "dst_obj1_a",
+					"obj2": map[string]interface{}{
+						"a": "dst_obj1_obj2_a",
+					},
+				},
+				"inline_str_1": "dst_inline_1",
+				"inline_str_2": "dst_inline_2",
+				"slice":        []interface{}{"dst_s1", "dst_s2", "dst_s3"},
+			},
+			map[string]interface{}{
+				"a": "dst_a",
+				"obj1": map[string]interface{}{
+					"a": "src_obj1_a",
+					"obj2": map[string]interface{}{
+						"a": "dst_obj1_obj2_a",
+					},
+				},
+				"inline_str_1": "src_inline_1",
+				"inline_str_2": "src_inline_2",
+				"slice":        []interface{}{"dst_s1", "src_s2", "dst_s3"},
+			},
+			false, false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			m := NewWalker(s, tt.config)
+			dst := tt.dst
+			res, _, err := m.DataWalk(context.Background(), dst, tt.src)
+			assert.Equal(t, tt.res, res)
+			if tt.wantErr {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}