diff --git a/go.mod b/go.mod
index 0efbbd17a96ea8fb3071e5b27820d68e4f2d80d3..64610c41c917cc60ebec0ceed850595620e3859c 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.18
 
 require (
 	github.com/antonmedv/expr v1.9.0
+	github.com/avast/retry-go v3.0.0+incompatible
 	github.com/go-kit/kit v0.12.0
 	github.com/golang/protobuf v1.5.2
 	github.com/gosimple/slug v1.13.1
diff --git a/go.sum b/go.sum
index e83ee408a6b4fea9e636d7028d7d8f0bf69f82f2..c23afb4718c63e0424aaa4805c160dd724735e56 100644
--- a/go.sum
+++ b/go.sum
@@ -5,6 +5,8 @@ cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2Aawl
 github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
 github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
+github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
+github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
diff --git a/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go b/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go
index 5402c513bc7ecbf3a7dc3b4a291282eed03f554e..cb9ee820f529f6ae1389415735dccbcdcd1bfee5 100644
--- a/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go
+++ b/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go
@@ -5,8 +5,8 @@
 package transportgrpc
 
 import (
-	pb "git.perx.ru/perxis/perxis-go/proto/environments"
 	service "git.perx.ru/perxis/perxis-go/pkg/environments"
+	pb "git.perx.ru/perxis/perxis-go/proto/environments"
 	"github.com/golang/protobuf/ptypes"
 )
 
diff --git a/pkg/environments/transport/server.microgen.go b/pkg/environments/transport/server.microgen.go
index 3a0b20a55cdf3aff82beb803efb59c5833e5afd7..d1b8264ad2a38989b549b3b30a8cd58e7c860554 100644
--- a/pkg/environments/transport/server.microgen.go
+++ b/pkg/environments/transport/server.microgen.go
@@ -3,8 +3,7 @@
 package transport
 
 import (
-
-"context"
+	"context"
 
 	"git.perx.ru/perxis/perxis-go/pkg/environments"
 	endpoint "github.com/go-kit/kit/endpoint"
diff --git a/pkg/extension/action.go b/pkg/extension/action.go
new file mode 100644
index 0000000000000000000000000000000000000000..3f090dffc541dcdee9f3dba7b0b8e6adc30931df
--- /dev/null
+++ b/pkg/extension/action.go
@@ -0,0 +1,213 @@
+package extension
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/references"
+	pb "git.perx.ru/perxis/perxis-go/proto/extensions"
+	"github.com/mitchellh/mapstructure"
+)
+
+const (
+	ActionsCollectionID   = "space_actions"
+	ActionsCollectionName = "Настройки/Действия"
+)
+
+type (
+	ActionKind    = pb.Action_Kind
+	ResponseState = pb.ActionResponse_State
+
+	ActionRequest  = pb.ActionRequest
+	ActionResponse = pb.ActionResponse
+
+	//ActionRequest struct {
+	//	Extension    string
+	//	Action       string
+	//	SpaceID      string
+	//	EnvID        string
+	//	CollectionID string
+	//	ItemID       string
+	//	ItemIDs      []string
+	//	Fields       []string
+	//	Metadata     map[string]string
+	//	Refs         []*references.Reference
+	//	Params       *references.Reference
+	//}
+	//
+	//ActionResponse struct {
+	//	State    ResponseState
+	//	Msg      string
+	//	Error    string
+	//	Next     []*Action
+	//	Metadata map[string]string
+	//	Refs     []*references.Reference
+	//}
+)
+
+const (
+	ResponseDone               = pb.ActionResponse_DONE
+	ResponseError              = pb.ActionResponse_ERROR
+	ResponsePending            = pb.ActionResponse_PENDING
+	ResponseInProgress         = pb.ActionResponse_IN_PROGRESS
+	ResponseParametersRequired = pb.ActionResponse_PARAMETERS_REQUIRED
+
+	ActionKindSpace       = pb.Action_SPACE
+	ActionKindEnvironment = pb.Action_ENVIRONMENT
+	ActionKindCollection  = pb.Action_COLLECTION
+	ActionKindItem        = pb.Action_ITEM
+	ActionKindItems       = pb.Action_ITEMS
+	ActionKindRevision    = pb.Action_REVISION
+	ActionKindCreate      = pb.Action_CREATE
+)
+
+type Action struct {
+	Extension        string                  `mapstructure:"extension,omitempty"`   // Расширение
+	Action           string                  `mapstructure:"action,omitempty"`      // Идентификатор действия
+	Name             string                  `mapstructure:"name,omitempty"`        // Название действия для отображения в интерфейсе (пункт меню, кнопка).
+	Description      string                  `mapstructure:"description,omitempty"` // Описание действия для отображения в интерфейсе
+	Icon             string                  `mapstructure:"icon,omitempty"`        // Название иконки для отображения действия в интерфейсе
+	Image            *references.Reference   `mapstructure:"image,omitempty"`       // Изображение для отображения в действия в интерфейсе
+	Groups           []string                `mapstructure:"groups,omitempty"`      // Группы отображения действия в интерфейсе
+	Kind             ActionKind              `mapstructure:"kind,omitempty"`        // Указывает на что направлено действие
+	Classes          []string                `mapstructure:"classes,omitempty"`     // Классы данных к которым применимо действие (название коллекций или специальных групп в рамках которых данное действие применимо)
+	Refs             []*references.Reference `mapstructure:"refs,omitempty"`        // Ссылки на записи используемые для выполнения действия (назначение ссылок зависит от действия и расширения)
+	ParamsCollection string                  `mapstructure:"params_collection,omitempty"`
+	Request          *ActionRequest          `mapstructure:"request,omitempty"`           // Параметры запроса (используется в случае `ActionResponse.next`)
+	NavigationAction bool                    `mapstructure:"navigation_action,omitempty"` // Флаг указывающий что действие переносить пользователя в другую часть интерфейса, а не отправляет запрос на сервер
+	NavigationRoute  string                  `mapstructure:"navigation_route,omitempty"`
+}
+
+func ActionToMap(action *Action) map[string]interface{} {
+	res := make(map[string]interface{})
+	mapstructure.Decode(action, &res)
+	res["kind"] = int64(action.Kind.Number())
+	return res
+}
+
+func ActionFromMap(d map[string]interface{}) (*Action, error) {
+	var action Action
+	err := mapstructure.Decode(d, &action)
+	return &action, err
+}
+
+//func ActionRequestToPB(req *ActionRequest) *pb.ActionRequest {
+//	if req == nil {
+//		return nil
+//	}
+//	return &pb.ActionRequest{
+//		Extension:    req.Extension,
+//		Action:       req.Action,
+//		SpaceId:      req.SpaceID,
+//		EnvId:        req.EnvID,
+//		CollectionId: req.CollectionID,
+//		ItemId:       req.ItemID,
+//		ItemIds:      req.ItemIDs,
+//		Fields:       req.Fields,
+//		Metadata:     req.Metadata,
+//		Refs:         references.ReferenceListToPB(req.Refs),
+//		Params:       references.ReferenceToPB(req.Params),
+//	}
+//}
+//
+//func ActionRequestFromPB(req *pb.ActionRequest) *ActionRequest {
+//	if req == nil {
+//		return nil
+//	}
+//	return &ActionRequest{
+//		Extension:    req.Extension,
+//		Action:       req.Action,
+//		SpaceID:      req.SpaceId,
+//		EnvID:        req.EnvId,
+//		CollectionID: req.CollectionId,
+//		ItemID:       req.ItemId,
+//		ItemIDs:      req.ItemIds,
+//		Fields:       req.Fields,
+//		Metadata:     req.Metadata,
+//		Refs:         references.ReferenceListFromPB(req.Refs),
+//		Params:       references.ReferenceFromPB(req.Params),
+//	}
+//}
+//func ActionResponseToPB(out *ActionResponse) *pb.ActionResponse {
+//	if out == nil {
+//		return nil
+//	}
+//
+//	var next []*pb.Action
+//	for _, a := range out.Next {
+//		next = append(next, ActionToPB(a))
+//	}
+//
+//	return &pb.ActionResponse{
+//		State:    out.State,
+//		Msg:      out.Msg,
+//		Error:    out.Error,
+//		Next:     next,
+//		Metadata: out.Metadata,
+//		Refs:     references.ReferenceListToPB(out.Refs),
+//	}
+//}
+//
+//func ActionResponseFromPB(out *pb.ActionResponse) *ActionResponse {
+//	if out == nil {
+//		return nil
+//	}
+//
+//	var next []*Action
+//	for _, a := range out.Next {
+//		next = append(next, ActionFromPB(a))
+//	}
+//
+//	return &ActionResponse{
+//		State:    out.State,
+//		Msg:      out.Msg,
+//		Error:    out.Error,
+//		Next:     next,
+//		Metadata: out.Metadata,
+//		Refs:     references.ReferenceListFromPB(out.Refs),
+//	}
+//}
+
+func ActionFromPB(a *pb.Action) *Action {
+	if a == nil {
+		return nil
+	}
+	return &Action{
+		Extension:        a.Extension,
+		Action:           a.Action,
+		Name:             a.Name,
+		Description:      a.Description,
+		Icon:             a.Icon,
+		Image:            references.ReferenceFromPB(a.Image),
+		Groups:           a.Groups,
+		Kind:             a.Kind,
+		Classes:          a.Classes,
+		Refs:             references.ReferenceListFromPB(a.Refs),
+		ParamsCollection: a.ParamsCollection,
+		Request:          a.Request,
+		//Request:          ActionRequestFromPB(a.Request),
+		NavigationAction: a.NavigationAction,
+		NavigationRoute:  a.NavigationRoute,
+	}
+}
+
+//
+//func ActionToPB(a *Action) *pb.Action {
+//	if a == nil {
+//		return nil
+//	}
+//	return &pb.Action{
+//		Extension:        a.Extension,
+//		Action:           a.Action,
+//		Name:             a.Name,
+//		Description:      a.Description,
+//		Icon:             a.Icon,
+//		Image:            references.ReferenceToPB(a.Image),
+//		Groups:           a.Groups,
+//		Kind:             a.Kind,
+//		Classes:          a.Classes,
+//		Refs:             references.ReferenceListToPB(a.Refs),
+//		ParamsCollection: a.ParamsCollection,
+//		Request:          a.Request,
+//		//Request:          ActionRequestToPB(a.Request),
+//		NavigationAction: a.NavigationAction,
+//		NavigationRoute:  a.NavigationRoute,
+//	}
+//}
diff --git a/pkg/extension/client.go b/pkg/extension/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..fc7276552f11ff086934e8d524541c77249d6d22
--- /dev/null
+++ b/pkg/extension/client.go
@@ -0,0 +1,82 @@
+package extension
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	pb "git.perx.ru/perxis/perxis-go/proto/extensions"
+	"google.golang.org/grpc"
+)
+
+type Client struct {
+	client pb.ExtensionClient
+}
+
+func NewClient(conn *grpc.ClientConn) *Client {
+	return &Client{
+		client: pb.NewExtensionClient(conn),
+	}
+}
+
+func (c Client) GetDescriptor() *ExtensionDescriptor {
+	return nil
+}
+
+type results interface {
+	GetResults() []*RequestResult
+}
+
+func getErrors(out results, err, wrapErr error) error {
+	if err != nil {
+		return err
+	}
+
+	res := out.GetResults()
+
+	var errs []error
+	for _, r := range res {
+		if r.State == RequestError {
+			err := errors.New(r.GetError())
+			if msg := r.GetMsg(); msg != "" {
+				err = errors.WithDetail(err, msg)
+			}
+			errs = append(errs, ExtensionError(err, r.Extension))
+		}
+
+		if r.UpdateAvailable {
+			errs = append(errs, ExtensionError(ErrUpdateAvailable, r.Extension))
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.WithErrors(wrapErr, errs...)
+	}
+
+	return nil
+}
+
+func (c Client) Install(ctx context.Context, in *InstallRequest) error {
+	out, err := c.client.Install(ctx, in)
+	return getErrors(out, err, ErrInstall)
+}
+
+func (c Client) Check(ctx context.Context, in *CheckRequest) error {
+	out, err := c.client.Check(ctx, in)
+	return getErrors(out, err, ErrCheck)
+}
+
+func (c Client) Update(ctx context.Context, in *UpdateRequest) error {
+	out, err := c.client.Update(ctx, in)
+	return getErrors(out, err, ErrUpdate)
+}
+
+func (c Client) Uninstall(ctx context.Context, in *UninstallRequest) error {
+	out, err := c.client.Uninstall(ctx, in)
+	return getErrors(out, err, ErrUninstall)
+}
+
+func (c Client) Action(ctx context.Context, in *ActionRequest) (*ActionResponse, error) {
+	out, err := c.client.Action(ctx, in)
+	return out, err
+	//return ActionResponseFromPB(out), err
+}
diff --git a/pkg/extension/client_test.go b/pkg/extension/client_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..d00032a5cb5f944a8dc4a3b6114d1fffaf14dbf7
--- /dev/null
+++ b/pkg/extension/client_test.go
@@ -0,0 +1,83 @@
+package extension
+
+import (
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetErrors(t *testing.T) {
+
+	getOut := func(results ...*RequestResult) results { return &InstallResponse{Results: results} }
+
+	tests := []struct {
+		name     string
+		out      results
+		err      error
+		wrapErr  error
+		checkErr func(t *testing.T, err error)
+	}{
+		{
+			name: "no errors",
+			out:  getOut(&RequestResult{State: RequestOK, Extension: "a"}),
+		},
+		{
+			name:    "no error",
+			out:     getOut(&RequestResult{State: RequestOK, Extension: "a"}),
+			wrapErr: ErrInstall,
+		},
+		{
+			name:    "one errored result",
+			out:     getOut(&RequestResult{State: RequestError, Extension: "a", Error: "some err", Msg: "Ошибка"}),
+			wrapErr: ErrInstall,
+			checkErr: func(t *testing.T, err error) {
+				assert.ErrorIs(t, err, ErrInstall)
+				errs := errors.GetErrors(err)
+				require.Len(t, errs, 1)
+
+				extErr := errs[0]
+				assert.Equal(t, "a", ExtensionFromError(extErr))
+				assert.Equal(t, "some err", extErr.Error())
+				assert.Equal(t, "Ошибка", errors.GetDetail(extErr))
+			},
+		},
+		{
+			name: "multiple results, some of them errored",
+			out: getOut(
+				&RequestResult{State: RequestError, Extension: "a", Error: "some err a", Msg: "Ошибка А"},
+				&RequestResult{State: RequestOK, Extension: "b"},
+				&RequestResult{State: RequestError, Extension: "c", Error: "some err c", Msg: "Ошибка С"},
+				&RequestResult{State: RequestOK, Extension: "d"},
+			),
+			wrapErr: ErrInstall,
+			checkErr: func(t *testing.T, err error) {
+				assert.ErrorIs(t, err, ErrInstall)
+				errs := errors.GetErrors(err)
+				require.Len(t, errs, 2)
+
+				extErr1 := errs[0]
+				assert.Equal(t, "a", ExtensionFromError(extErr1))
+				assert.Equal(t, "some err a", extErr1.Error())
+				assert.Equal(t, "Ошибка А", errors.GetDetail(extErr1))
+
+				extErr2 := errs[1]
+				assert.Equal(t, "c", ExtensionFromError(extErr2))
+				assert.Equal(t, "some err c", extErr2.Error())
+				assert.Equal(t, "Ошибка С", errors.GetDetail(extErr2))
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := getErrors(tt.out, tt.err, tt.wrapErr)
+			if tt.checkErr != nil {
+				require.Error(t, err)
+				tt.checkErr(t, err)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go
new file mode 100644
index 0000000000000000000000000000000000000000..26d9b6d51171307161e68ceef0830857fbb569a8
--- /dev/null
+++ b/pkg/extension/extension.go
@@ -0,0 +1,105 @@
+package extension
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	pb "git.perx.ru/perxis/perxis-go/proto/extensions"
+)
+
+const (
+	RequestOK    = pb.ExtensionRequestResult_OK
+	RequestError = pb.ExtensionRequestResult_ERROR
+
+	ExtensionsCollectionID   = "space_extensions"
+	ExtensionsCollectionName = "Настройки/Расширения"
+
+	StatePending    = pb.SpaceExtensions_PENDING
+	StateInstalled  = pb.SpaceExtensions_INSTALLED
+	StateInProgress = pb.SpaceExtensions_IN_PROGRESS
+	StateFail       = pb.SpaceExtensions_FAIL
+)
+
+type (
+	InstallRequest    = pb.InstallRequest
+	InstallResponse   = pb.InstallResponse
+	CheckRequest      = pb.CheckRequest
+	CheckResponse     = pb.CheckResponse
+	UpdateRequest     = pb.UpdateRequest
+	UpdateResponse    = pb.UpdateResponse
+	UninstallRequest  = pb.UninstallRequest
+	UninstallResponse = pb.UninstallResponse
+	RequestResult     = pb.ExtensionRequestResult
+
+	ExtensionDescriptor  = pb.ExtensionDescriptor
+	SpaceExtensionsState = pb.SpaceExtensions_State
+)
+
+var (
+	ErrStart = errors.New("start failed")
+	ErrStop  = errors.New("stop failed")
+
+	ErrInstall   = errors.New("install failed")
+	ErrUpdate    = errors.New("update failed")
+	ErrCheck     = errors.New("check failed")
+	ErrUninstall = errors.New("uninstall failed")
+
+	ErrUpdateAvailable  = errors.New("update available")
+	ErrNotInstalled     = errors.New("not installed")
+	ErrUnknownExtension = errors.New("unknown extension")
+)
+
+// Runnable описывает интерфейс сервиса с запуском и остановкой. Вызывается сервером расширений
+type Runnable interface {
+	Start() error
+	Stop() error
+}
+
+// Extension описывает интерфейс расширения Perxis
+type Extension interface {
+	// GetDescriptor возвращает описание расширения
+	GetDescriptor() *ExtensionDescriptor
+
+	// Install вызывается при установке расширения в пространство
+	Install(ctx context.Context, in *InstallRequest) error
+
+	// Check вызывается для проверки состояния расширения
+	Check(ctx context.Context, in *CheckRequest) error
+
+	// Update вызывается для обновления вресии расширения
+	Update(ctx context.Context, in *UpdateRequest) error
+
+	// Uninstall вызывается для удаления расширения из пространства
+	Uninstall(ctx context.Context, in *UninstallRequest) error
+
+	// Action вызывается для выполнения расширением действия
+	Action(ctx context.Context, in *ActionRequest) (*ActionResponse, error)
+}
+
+func CheckInstalled(ctx context.Context, content *content.Content, spaceID, envID, extension string) (bool, error) {
+	res, _, err := content.Items.Find(ctx, spaceID, envID, ExtensionsCollectionID,
+		&items.Filter{Q: []string{fmt.Sprintf("extension == '%s'", extension)}})
+
+	if err != nil {
+		return false, err
+	}
+
+	if len(res) == 0 || res[0].Data["extension_state"] != int64(StateInstalled) {
+		return false, nil
+	}
+
+	return true, nil
+}
+
+func ExtensionError(err error, ext string) error {
+	return errors.WithContext(err, "extension", ext)
+}
+
+func ExtensionFromError(err error) string {
+	v, _ := errors.ContextKey(err, "extension")
+	ext, _ := v.(string)
+	return ext
+}
diff --git a/pkg/extension/key.go b/pkg/extension/key.go
new file mode 100644
index 0000000000000000000000000000000000000000..a93bafe2fc72fd39e80d617fc548512f6f61e663
--- /dev/null
+++ b/pkg/extension/key.go
@@ -0,0 +1,55 @@
+package extension
+
+import (
+	"crypto/sha512"
+	"encoding/base64"
+	"strings"
+)
+
+// KeyFunc функция получения ключа доступа к указанному пространству
+type KeyFunc func(spaceID string) string
+type SignatureFunc func(spaceID string) string
+
+func NamedSignedKey(name string, signature any) KeyFunc {
+	switch v := signature.(type) {
+	case string:
+		return func(spaceID string) string {
+			return Sign(name, spaceID, v)
+		}
+	case SignatureFunc:
+		return func(spaceID string) string {
+			return Sign(name, spaceID, v(spaceID))
+		}
+	}
+	panic("incorrect signature")
+}
+
+func ExtensionSignedKey(ext, signature any) KeyFunc {
+	var desc *ExtensionDescriptor
+
+	switch v := ext.(type) {
+	case Extension:
+		desc = v.GetDescriptor()
+	case *ExtensionDescriptor:
+		desc = v
+	}
+
+	switch v := signature.(type) {
+	case string:
+		return func(spaceID string) string {
+			return Sign(desc.Extension, spaceID, v)
+		}
+	case SignatureFunc:
+		return func(spaceID string) string {
+			return Sign(desc.Extension, spaceID, v(spaceID))
+		}
+	}
+	panic("incorrect signature")
+}
+
+func Sign(vals ...string) string {
+	value := strings.Join(vals, ".")
+	hash := sha512.Sum512([]byte(value))
+	encoded := base64.StdEncoding.EncodeToString(hash[:])
+	return encoded
+}
diff --git a/pkg/extension/manager.go b/pkg/extension/manager.go
new file mode 100644
index 0000000000000000000000000000000000000000..958ac1788315fdaf4f8f4776099888a5af7b1b1d
--- /dev/null
+++ b/pkg/extension/manager.go
@@ -0,0 +1,81 @@
+package extension
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	pb "git.perx.ru/perxis/perxis-go/proto/extensions"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+)
+
+type Manager interface {
+	Extension
+	RegisterExtensions(ctx context.Context, ext ...*ExtensionConnector) error
+	UnregisterExtensions(ctx context.Context, ext ...*ExtensionConnector) error
+	ListExtensions(ctx context.Context, filter *ListExtensionsFilter) ([]*ExtensionConnector, error)
+}
+
+type (
+	ListExtensionsFilter = pb.ListExtensionsFilter
+)
+
+type ExtensionConnector struct {
+	Extension
+	Connection *grpc.ClientConn
+	Descriptor *ExtensionDescriptor
+}
+
+func (e *ExtensionConnector) Connect() error {
+	// Уже есть экземпляр расширение или клиент расширения
+	if e.Extension != nil {
+		return nil
+	}
+
+	// Уже есть соединение, создаем клиента расширения
+	if e.Connection != nil {
+		if e.Extension == nil {
+			e.Extension = NewClient(e.Connection)
+			return nil
+		}
+		return nil
+	}
+
+	// Нет дескриптора или Url для соединения с расширением
+	if e.Descriptor == nil || e.Descriptor.Url == "" {
+		return nil
+	}
+
+	conn, err := grpc.Dial(
+		e.Descriptor.Url,
+		grpc.WithTransportCredentials(insecure.NewCredentials()),
+		grpc.WithChainUnaryInterceptor(
+			auth.PrincipalClientInterceptor(),
+		),
+	)
+	if err != nil {
+		return err
+	}
+
+	e.Connection = conn
+	e.Extension = NewClient(conn)
+	return nil
+}
+
+func (e *ExtensionConnector) IsConnected() bool {
+	if e != nil && e.Connection != nil {
+		return true
+	}
+	return false
+}
+
+func (e *ExtensionConnector) Close() error {
+	if e.IsConnected() {
+		if err := e.Connection.Close(); err != nil {
+			return errors.Wrapf(err, "fail to close connection, extension %s (%s)", e.Extension, e.Descriptor.Url)
+		}
+		e.Connection = nil
+	}
+	return nil
+}
diff --git a/pkg/extension/manager_client.go b/pkg/extension/manager_client.go
new file mode 100644
index 0000000000000000000000000000000000000000..99057c3e75f22cb623d7aeff83ac0f9016d9383e
--- /dev/null
+++ b/pkg/extension/manager_client.go
@@ -0,0 +1,72 @@
+package extension
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	pb "git.perx.ru/perxis/perxis-go/proto/extensions"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+)
+
+type ManagerClient struct {
+	*Client
+	manager pb.ExtensionManagerClient
+	conn    *grpc.ClientConn
+}
+
+func NewManagerClientWithAddr(addr string) (*ManagerClient, error) {
+	cc, err := grpc.Dial(addr,
+		grpc.WithTransportCredentials(insecure.NewCredentials()),
+		grpc.WithUnaryInterceptor(auth.PrincipalClientInterceptor()),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewManagerClient(cc), nil
+}
+
+func NewManagerClient(cc *grpc.ClientConn) *ManagerClient {
+	return &ManagerClient{
+		manager: pb.NewExtensionManagerClient(cc),
+		conn:    cc,
+		Client:  NewClient(cc),
+	}
+}
+
+func (c *ManagerClient) Close() error {
+	return c.conn.Close()
+}
+
+func (c *ManagerClient) RegisterExtensions(ctx context.Context, exts ...*ExtensionConnector) error {
+	var descs []*pb.ExtensionDescriptor
+	for _, e := range exts {
+		descs = append(descs, e.Descriptor)
+	}
+	_, err := c.manager.RegisterExtensions(ctx, &pb.RegisterExtensionsRequest{Extensions: descs}, grpc.WaitForReady(true))
+	return err
+}
+
+func (c *ManagerClient) UnregisterExtensions(ctx context.Context, exts ...*ExtensionConnector) error {
+	var descs []*pb.ExtensionDescriptor
+	for _, e := range exts {
+		descs = append(descs, e.Descriptor)
+	}
+	_, err := c.manager.UnregisterExtensions(ctx, &pb.UnregisterExtensionsRequest{Extensions: descs}, grpc.WaitForReady(true))
+	return err
+}
+
+func (c *ManagerClient) ListExtensions(ctx context.Context, filter *ListExtensionsFilter) ([]*ExtensionConnector, error) {
+	resp, err := c.manager.ListExtensions(ctx, &pb.ListExtensionsRequest{Filter: filter}, grpc.WaitForReady(true))
+	if err != nil {
+		return nil, err
+	}
+
+	var exts []*ExtensionConnector
+	for _, desc := range resp.Extensions {
+		exts = append(exts, &ExtensionConnector{Descriptor: desc})
+	}
+
+	return exts, nil
+}
diff --git a/pkg/extension/mocks/Extension.go b/pkg/extension/mocks/Extension.go
new file mode 100644
index 0000000000000000000000000000000000000000..87710ccff1d98aa5e0922d7e1563caad8c9f21f1
--- /dev/null
+++ b/pkg/extension/mocks/Extension.go
@@ -0,0 +1,126 @@
+// Code generated by mockery v2.14.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	extensions "git.perx.ru/perxis/perxis-go/proto/extensions"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Extension is an autogenerated mock type for the Extension type
+type Extension struct {
+	mock.Mock
+}
+
+// Action provides a mock function with given fields: ctx, in
+func (_m *Extension) Action(ctx context.Context, in *extensions.ActionRequest) (*extensions.ActionResponse, error) {
+	ret := _m.Called(ctx, in)
+
+	var r0 *extensions.ActionResponse
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.ActionRequest) *extensions.ActionResponse); ok {
+		r0 = rf(ctx, in)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*extensions.ActionResponse)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *extensions.ActionRequest) error); ok {
+		r1 = rf(ctx, in)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Check provides a mock function with given fields: ctx, in
+func (_m *Extension) Check(ctx context.Context, in *extensions.CheckRequest) error {
+	ret := _m.Called(ctx, in)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.CheckRequest) error); ok {
+		r0 = rf(ctx, in)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// GetDescriptor provides a mock function with given fields:
+func (_m *Extension) GetDescriptor() *extensions.ExtensionDescriptor {
+	ret := _m.Called()
+
+	var r0 *extensions.ExtensionDescriptor
+	if rf, ok := ret.Get(0).(func() *extensions.ExtensionDescriptor); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*extensions.ExtensionDescriptor)
+		}
+	}
+
+	return r0
+}
+
+// Install provides a mock function with given fields: ctx, in
+func (_m *Extension) Install(ctx context.Context, in *extensions.InstallRequest) error {
+	ret := _m.Called(ctx, in)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.InstallRequest) error); ok {
+		r0 = rf(ctx, in)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Uninstall provides a mock function with given fields: ctx, in
+func (_m *Extension) Uninstall(ctx context.Context, in *extensions.UninstallRequest) error {
+	ret := _m.Called(ctx, in)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.UninstallRequest) error); ok {
+		r0 = rf(ctx, in)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, in
+func (_m *Extension) Update(ctx context.Context, in *extensions.UpdateRequest) error {
+	ret := _m.Called(ctx, in)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.UpdateRequest) error); ok {
+		r0 = rf(ctx, in)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewExtension interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewExtension creates a new instance of Extension. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewExtension(t mockConstructorTestingTNewExtension) *Extension {
+	mock := &Extension{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/extension/mocks/Manager.go b/pkg/extension/mocks/Manager.go
new file mode 100644
index 0000000000000000000000000000000000000000..9dd78407842bb31086066b1ebbee6fb2de180484
--- /dev/null
+++ b/pkg/extension/mocks/Manager.go
@@ -0,0 +1,192 @@
+// Code generated by mockery v2.14.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	extension "git.perx.ru/perxis/perxis-go/pkg/extension"
+	extensions "git.perx.ru/perxis/perxis-go/proto/extensions"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Manager is an autogenerated mock type for the Manager type
+type Manager struct {
+	mock.Mock
+}
+
+// Action provides a mock function with given fields: ctx, in
+func (_m *Manager) Action(ctx context.Context, in *extensions.ActionRequest) (*extensions.ActionResponse, error) {
+	ret := _m.Called(ctx, in)
+
+	var r0 *extensions.ActionResponse
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.ActionRequest) *extensions.ActionResponse); ok {
+		r0 = rf(ctx, in)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*extensions.ActionResponse)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *extensions.ActionRequest) error); ok {
+		r1 = rf(ctx, in)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Check provides a mock function with given fields: ctx, in
+func (_m *Manager) Check(ctx context.Context, in *extensions.CheckRequest) error {
+	ret := _m.Called(ctx, in)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.CheckRequest) error); ok {
+		r0 = rf(ctx, in)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// GetDescriptor provides a mock function with given fields:
+func (_m *Manager) GetDescriptor() *extensions.ExtensionDescriptor {
+	ret := _m.Called()
+
+	var r0 *extensions.ExtensionDescriptor
+	if rf, ok := ret.Get(0).(func() *extensions.ExtensionDescriptor); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*extensions.ExtensionDescriptor)
+		}
+	}
+
+	return r0
+}
+
+// Install provides a mock function with given fields: ctx, in
+func (_m *Manager) Install(ctx context.Context, in *extensions.InstallRequest) error {
+	ret := _m.Called(ctx, in)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.InstallRequest) error); ok {
+		r0 = rf(ctx, in)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// ListExtensions provides a mock function with given fields: ctx, filter
+func (_m *Manager) ListExtensions(ctx context.Context, filter *extensions.ListExtensionsFilter) ([]*extension.ExtensionConnector, error) {
+	ret := _m.Called(ctx, filter)
+
+	var r0 []*extension.ExtensionConnector
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.ListExtensionsFilter) []*extension.ExtensionConnector); ok {
+		r0 = rf(ctx, filter)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*extension.ExtensionConnector)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *extensions.ListExtensionsFilter) error); ok {
+		r1 = rf(ctx, filter)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// RegisterExtensions provides a mock function with given fields: ctx, ext
+func (_m *Manager) RegisterExtensions(ctx context.Context, ext ...*extension.ExtensionConnector) error {
+	_va := make([]interface{}, len(ext))
+	for _i := range ext {
+		_va[_i] = ext[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, ...*extension.ExtensionConnector) error); ok {
+		r0 = rf(ctx, ext...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Uninstall provides a mock function with given fields: ctx, in
+func (_m *Manager) Uninstall(ctx context.Context, in *extensions.UninstallRequest) error {
+	ret := _m.Called(ctx, in)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.UninstallRequest) error); ok {
+		r0 = rf(ctx, in)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// UnregisterExtensions provides a mock function with given fields: ctx, ext
+func (_m *Manager) UnregisterExtensions(ctx context.Context, ext ...*extension.ExtensionConnector) error {
+	_va := make([]interface{}, len(ext))
+	for _i := range ext {
+		_va[_i] = ext[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, ...*extension.ExtensionConnector) error); ok {
+		r0 = rf(ctx, ext...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, in
+func (_m *Manager) Update(ctx context.Context, in *extensions.UpdateRequest) error {
+	ret := _m.Called(ctx, in)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *extensions.UpdateRequest) error); ok {
+		r0 = rf(ctx, in)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewManager interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewManager(t mockConstructorTestingTNewManager) *Manager {
+	mock := &Manager{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/extension/server.go b/pkg/extension/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..f683d0914d897de21dc052f93734c6604e69ced9
--- /dev/null
+++ b/pkg/extension/server.go
@@ -0,0 +1,136 @@
+package extension
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	pb "git.perx.ru/perxis/perxis-go/proto/extensions"
+)
+
+type Server struct {
+	services map[string]Extension
+	pb.UnimplementedExtensionServer
+}
+
+func NewServer(svc ...Extension) *Server {
+	srv := &Server{
+		services: make(map[string]Extension, len(svc)),
+	}
+	for _, s := range svc {
+		srv.services[s.GetDescriptor().Extension] = s
+	}
+	return srv
+}
+
+func getResult(ext string, err error) *RequestResult {
+	res := new(RequestResult)
+	res.Extension = ext
+	res.State = RequestOK
+
+	if err != nil {
+		res.State = RequestError
+		res.Error = err.Error()
+		errs := errors.GetErrors(err)
+		if errs == nil {
+			errs = append(errs, err)
+		}
+		for _, e := range errs {
+			res.Msg += errors.GetDetail(e) + "\n"
+		}
+	}
+	return res
+}
+
+func (srv *Server) getResults(extensions []string, fn func(svc Extension) error) []*RequestResult {
+
+	var results []*RequestResult
+
+	for _, e := range extensions {
+		svc, ok := srv.services[e]
+		if !ok {
+			results = append(results, getResult(e, ErrUnknownExtension))
+			continue
+		}
+
+		err := fn(svc)
+		results = append(results, getResult(e, err))
+	}
+
+	return results
+}
+
+func (srv *Server) Install(ctx context.Context, request *InstallRequest) (*InstallResponse, error) {
+	res := srv.getResults(request.Extensions, func(svc Extension) error { return svc.Install(ctx, request) })
+	return &InstallResponse{Results: res}, nil
+}
+
+func (srv *Server) Check(ctx context.Context, request *CheckRequest) (*CheckResponse, error) {
+	res := srv.getResults(request.Extensions, func(svc Extension) error { return svc.Check(ctx, request) })
+	return &CheckResponse{Results: res}, nil
+}
+
+func (srv *Server) Uninstall(ctx context.Context, request *UninstallRequest) (*UninstallResponse, error) {
+	res := srv.getResults(request.Extensions, func(svc Extension) error { return svc.Uninstall(ctx, request) })
+	return &UninstallResponse{Results: res}, nil
+}
+
+func (srv *Server) Update(ctx context.Context, request *UpdateRequest) (*UpdateResponse, error) {
+	res := srv.getResults(request.Extensions, func(svc Extension) error { return svc.Update(ctx, request) })
+	return &UpdateResponse{Results: res}, nil
+}
+
+func (srv *Server) Action(ctx context.Context, in *pb.ActionRequest) (*pb.ActionResponse, error) {
+
+	svc, ok := srv.services[in.Extension]
+	if !ok {
+		return nil, ErrUnknownExtension
+	}
+
+	out, err := svc.Action(ctx, in)
+
+	if out == nil {
+		out = &ActionResponse{}
+	}
+
+	if err != nil {
+		out.State = ResponseError
+		out.Error = err.Error()
+		out.Msg += errors.GetDetail(err)
+	}
+
+	return out, nil
+}
+
+func (srv *Server) Start() error {
+	var errs []error
+	for _, svc := range srv.services {
+		if r, ok := svc.(Runnable); ok {
+			if err := r.Start(); err != nil {
+				errs = append(errs, err)
+			}
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.WithErrors(ErrStart, errs...)
+	}
+
+	return nil
+}
+
+func (srv *Server) Stop() error {
+	var errs []error
+	for _, svc := range srv.services {
+		if r, ok := svc.(Runnable); ok {
+			if err := r.Stop(); err != nil {
+				errs = append(errs, err)
+			}
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.WithErrors(ErrStop, errs...)
+	}
+
+	return nil
+}
diff --git a/pkg/extension/server_test.go b/pkg/extension/server_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..5400c8c4786cbe09bf2c56fdfb0611d65048d31d
--- /dev/null
+++ b/pkg/extension/server_test.go
@@ -0,0 +1,104 @@
+package extension
+
+import (
+	"context"
+	"reflect"
+	"strings"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+)
+
+func TestGetResults(t *testing.T) {
+
+	getDummyExtension := func(name string, wantErr ...bool) Extension {
+		ext := &testServerExtension{name: name}
+
+		if len(wantErr) > 0 {
+			ext.err = errors.WithDetail(errors.New("some err"), "Ошибка")
+		}
+
+		return ext
+	}
+
+	tests := []struct {
+		name       string
+		services   []Extension
+		extensions []string
+		fn         func(svc Extension) error
+		want       []*RequestResult
+	}{
+		{
+			name:       "one extension without errors",
+			services:   []Extension{getDummyExtension("a"), getDummyExtension("b")},
+			extensions: []string{"a"},
+			fn:         func(svc Extension) error { return nil },
+			want: []*RequestResult{
+				{Extension: "a", State: RequestOK},
+			},
+		},
+		{
+			name:       "multiple extensions without errors",
+			services:   []Extension{getDummyExtension("a"), getDummyExtension("b"), getDummyExtension("c")},
+			extensions: []string{"a", "c"},
+			fn:         func(svc Extension) error { return nil },
+			want: []*RequestResult{
+				{Extension: "a", State: RequestOK},
+				{Extension: "c", State: RequestOK},
+			},
+		},
+		{
+			name:       "multiple extensions, one returns error",
+			services:   []Extension{getDummyExtension("a"), getDummyExtension("b"), getDummyExtension("c", true)},
+			extensions: []string{"a", "c"},
+			fn:         func(svc Extension) error { return svc.Install(nil, nil) },
+			want: []*RequestResult{
+				{Extension: "a", State: RequestOK},
+				{Extension: "c", State: RequestError, Error: "some err", Msg: "Ошибка\n"},
+			},
+		},
+		{
+			name:       "multiple extensions, all return error",
+			services:   []Extension{getDummyExtension("a", true), getDummyExtension("b", true), getDummyExtension("c", true)},
+			extensions: []string{"a", "b", "c"},
+			fn:         func(svc Extension) error { return svc.Install(nil, nil) },
+			want: []*RequestResult{
+				{Extension: "a", State: RequestError, Error: "some err", Msg: "Ошибка\n"},
+				{Extension: "b", State: RequestError, Error: "some err", Msg: "Ошибка\n"},
+				{Extension: "c", State: RequestError, Error: "some err", Msg: "Ошибка\n"},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			srv := NewServer(tt.services...)
+			if got := srv.getResults(tt.extensions, tt.fn); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("getResults() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+// не подходит использование mock.Extension из-за возникающих циклических импортов
+type testServerExtension struct {
+	err  error
+	name string
+}
+
+func (t testServerExtension) GetDescriptor() *ExtensionDescriptor {
+	return &ExtensionDescriptor{
+		Extension:   t.name,
+		Title:       strings.ToTitle(t.name),
+		Description: "test extension",
+		Version:     "0.0.0",
+	}
+}
+
+func (t testServerExtension) Install(ctx context.Context, in *InstallRequest) error     { return t.err }
+func (t testServerExtension) Check(ctx context.Context, in *CheckRequest) error         { return t.err }
+func (t testServerExtension) Update(ctx context.Context, in *UpdateRequest) error       { return t.err }
+func (t testServerExtension) Uninstall(ctx context.Context, in *UninstallRequest) error { return t.err }
+func (t testServerExtension) Action(ctx context.Context, in *ActionRequest) (*ActionResponse, error) {
+	return &ActionResponse{}, t.err
+}
diff --git a/pkg/extension/service/doc.go b/pkg/extension/service/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..c834c26e1dcffff9ae61aa7fa644c10cccd1a621
--- /dev/null
+++ b/pkg/extension/service/doc.go
@@ -0,0 +1,20 @@
+package service
+
+/*
+
+Пакет содержит инструментарий для построения расширений
+
+
+Для реализации расширения сервисом нужно включение реализации расширения.
+
+Для автоматизации регистрации расширений
+реализуемых сервисом требуется включение регистратора.
+
+```
+type Service struct {
+	*service.Extension
+	*service.Registrar
+}
+```
+
+*/
diff --git a/pkg/extension/service/extension.go b/pkg/extension/service/extension.go
new file mode 100644
index 0000000000000000000000000000000000000000..1ee716d9c1d8066b2e1c5a6adbd082382ae30dc0
--- /dev/null
+++ b/pkg/extension/service/extension.go
@@ -0,0 +1,167 @@
+package service
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/extension"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"git.perx.ru/perxis/perxis-go/pkg/setup"
+	"go.uber.org/zap"
+)
+
+const (
+	roleID     = "%s_extension"
+	roleDesc   = "Роль расширения \"%s\""
+	clientID   = "%s_extension"
+	clientName = "Клиент расширения \"%s\""
+	clientDesc = "Клиент используется при работе расширения \"%s\""
+)
+
+type SetupFunc func(spaceID, envID string) *setup.Setup
+type SignatureFunc func(spaceID string) string
+
+// Extension реализация сервиса с одним расширением
+type Extension struct {
+	desc      *extension.ExtensionDescriptor
+	setupFunc SetupFunc
+	Content   *content.Content
+	Logger    *zap.Logger
+	manager   extension.Manager
+
+	withClient bool
+	role       *roles.Role
+	client     *clients.Client
+	keyFn      extension.KeyFunc
+}
+
+func NewExtension(desc *extension.ExtensionDescriptor, cnt *content.Content, setupFunc SetupFunc, logger *zap.Logger) *Extension {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+	return &Extension{
+		desc:      desc,
+		setupFunc: setupFunc,
+		Content:   cnt,
+		Logger:    logger,
+	}
+}
+
+func (s *Extension) GetDescriptor() *extension.ExtensionDescriptor {
+	return s.desc
+}
+
+func (s *Extension) GetName() string {
+	return s.desc.Extension
+}
+
+// WithClient добавляет в процесс установки расширения создание роли и приложения для работы расширения
+func (s *Extension) WithClient(role *roles.Role, client *clients.Client, fn extension.KeyFunc) *Extension {
+	s.withClient = true
+	s.keyFn = fn
+	s.role = role
+	s.client = client
+	return s
+}
+
+// GetKey Получить ключ для доступа к пространству
+func (s *Extension) GetKey(spaceID string) string {
+	if s == nil || s.keyFn == nil {
+		return ""
+	}
+
+	return s.keyFn(spaceID)
+}
+
+// setupExtensionClient добавляет роль и клиента для авторизации расширения
+func (s *Extension) setupExtensionClient(set *setup.Setup, spaceID string) {
+	if !s.withClient {
+		return
+	}
+
+	if s.role == nil {
+		s.role = &roles.Role{}
+	}
+
+	if s.role.ID == "" {
+		s.role.ID = fmt.Sprintf(roleID, s.desc.Extension)
+	}
+
+	if s.role.Description == "" {
+		s.role.Description = fmt.Sprintf(roleDesc, s.desc.Title)
+	}
+
+	role := *s.role
+	role.SpaceID = spaceID
+
+	set.AddRole(&role, setup.DeleteRoleIfRemove())
+
+	if s.client == nil {
+		s.client = &clients.Client{}
+	}
+
+	if s.client.ID == "" {
+		s.client.ID = fmt.Sprintf(clientID, s.desc.Extension)
+	}
+	if s.client.RoleID == "" {
+		s.client.RoleID = s.role.ID
+	}
+	if s.client.Name == "" {
+		s.client.Name = fmt.Sprintf(clientName, s.desc.Title)
+	}
+	if s.client.Description == "" {
+		s.client.Description = fmt.Sprintf(clientDesc, s.desc.Description)
+	}
+	if s.client.OAuth == nil {
+		s.client.OAuth = &clients.OAuth{
+			ClientID: fmt.Sprintf(clientID, s.desc.Extension),
+		}
+	}
+
+	client := *s.client
+	client.SpaceID = spaceID
+	if client.APIKey == nil {
+		client.APIKey = &clients.APIKey{
+			Key: s.GetKey(spaceID),
+		}
+	}
+
+	set.AddClient(&client, setup.OverwriteClient(), setup.DeleteClientIfRemove())
+}
+
+func (s *Extension) GetSetup(spaceID, envID string) *setup.Setup {
+	set := s.setupFunc(spaceID, envID)
+	s.setupExtensionClient(set, spaceID)
+	return set
+}
+
+func (s *Extension) Install(ctx context.Context, in *extension.InstallRequest) error {
+	return s.GetSetup(in.SpaceId, in.EnvId).WithForce(in.Force).Install(ctx)
+}
+
+func (s *Extension) Check(ctx context.Context, in *extension.CheckRequest) error {
+	return s.GetSetup(in.SpaceId, in.EnvId).Check(ctx)
+}
+
+func (s *Extension) Update(ctx context.Context, in *extension.UpdateRequest) error {
+	return s.GetSetup(in.SpaceId, in.EnvId).WithForce(in.Force).Install(ctx)
+}
+
+func (s *Extension) Uninstall(ctx context.Context, in *extension.UninstallRequest) error {
+	return s.GetSetup(in.SpaceId, in.EnvId).WithForce(in.Force).WithRemove(in.Remove).Uninstall(ctx)
+}
+
+func (s *Extension) Action(ctx context.Context, in *extension.ActionRequest) (*extension.ActionResponse, error) {
+	ok, err := extension.CheckInstalled(ctx, s.Content, in.SpaceId, in.EnvId, in.Extension)
+	if err != nil {
+		return nil, errors.Wrap(err, "check extension installed")
+	}
+	if !ok {
+		return nil, errors.New("extension not installed")
+	}
+
+	return &extension.ActionResponse{}, nil
+}
diff --git a/pkg/extension/service/registrar.go b/pkg/extension/service/registrar.go
new file mode 100644
index 0000000000000000000000000000000000000000..5d505d2667a3b752cd4229348f9bfbbeab9f74fb
--- /dev/null
+++ b/pkg/extension/service/registrar.go
@@ -0,0 +1,98 @@
+package service
+
+import (
+	"context"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/extension"
+	"github.com/avast/retry-go"
+	"go.uber.org/zap"
+	"google.golang.org/grpc"
+)
+
+const RegistrationDelay = time.Minute
+
+// Registrar выполняет действия по регистрации и обновления регистрации расширений в менеджере расширений. Одновременно
+// выполняется регистрация одного или нескольких расширений
+type Registrar struct {
+	addr        string
+	managerConn *grpc.ClientConn
+	manager     extension.Manager
+	exts        []extension.Extension
+	logger      *zap.Logger
+	stopFn      func() error
+}
+
+func NewRegistrar(addr string, man extension.Manager, exts []extension.Extension, logger *zap.Logger) *Registrar {
+	return &Registrar{
+		addr:    addr,
+		manager: man,
+		exts:    exts,
+		logger:  logger,
+	}
+}
+
+func (reg *Registrar) register(ctx context.Context, manager extension.Manager, desc []*extension.ExtensionConnector) error {
+	err := retry.Do(
+		func() error {
+			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+			defer cancel()
+			err := manager.RegisterExtensions(auth.WithSystem(ctx), desc...)
+			return err
+		},
+		retry.RetryIf(func(err error) bool { return err != nil }),
+		retry.OnRetry(func(n uint, err error) {
+			reg.logger.Warn("Fail to register extension", zap.Uint("Retry", n), zap.Error(err))
+		}),
+		retry.DelayType(retry.BackOffDelay),
+		retry.MaxDelay(2*time.Minute),
+		retry.Attempts(1000),
+		retry.Context(ctx),
+	)
+
+	if err == nil {
+		reg.logger.Debug("Extensions successful registered")
+	}
+
+	return err
+}
+
+func (reg *Registrar) Start() error {
+	registrationDelay := time.Duration(0)
+
+	regCtx, regStop := context.WithCancel(context.Background())
+
+	reg.stopFn = func() error {
+		regStop()
+		return nil
+	}
+
+	extList := make([]*extension.ExtensionConnector, 0, len(reg.exts))
+	for _, v := range reg.exts {
+		desc := *v.GetDescriptor()
+		desc.Url = reg.addr
+		extList = append(extList, &extension.ExtensionConnector{Descriptor: &desc})
+	}
+
+	reg.logger.Info("Start registration process")
+
+	go func() {
+		for {
+			select {
+			case <-time.After(registrationDelay):
+				reg.register(regCtx, reg.manager, extList)
+				registrationDelay = RegistrationDelay
+			case <-regCtx.Done():
+				reg.logger.Info("Stop registration process")
+				return
+			}
+		}
+	}()
+
+	return nil
+}
+
+func (reg *Registrar) Stop() error {
+	return reg.stopFn()
+}
diff --git a/pkg/items/options.go b/pkg/items/options.go
index 3d45a226ac44f858cf12c6df3e1eafdb1a3b38fa..0c6bd9094ff763f0e2df03fcb2525cd5555a44eb 100644
--- a/pkg/items/options.go
+++ b/pkg/items/options.go
@@ -215,7 +215,7 @@ func MergePublishOptions(opts ...*PublishOptions) *PublishOptions {
 
 type UnpublishOptions struct {
 	UpdateAttrs bool
-	
+
 	Options
 }
 
diff --git a/pkg/items/transport/client.go b/pkg/items/transport/client.go
index db1a7a789df8bf7b820d018a219bcd4d3a635153..fd5de3f21b7ad9ec0bb1eed5f0a908216c9531d7 100644
--- a/pkg/items/transport/client.go
+++ b/pkg/items/transport/client.go
@@ -90,7 +90,7 @@ func (set EndpointsSet) Update(arg0 context.Context, arg1 *items.Item, arg2 ...*
 }
 
 func (set EndpointsSet) Delete(arg0 context.Context, arg1 *items.Item, arg2 ...*items.DeleteOptions) (res0 error) {
-	request := DeleteRequest{Item: arg1,Options: arg2}
+	request := DeleteRequest{Item: arg1, Options: arg2}
 	_, res0 = set.DeleteEndpoint(arg0, &request)
 	if res0 != nil {
 		return
@@ -98,7 +98,7 @@ func (set EndpointsSet) Delete(arg0 context.Context, arg1 *items.Item, arg2 ...*
 	return res0
 }
 
-func (set EndpointsSet) Undelete(arg0 context.Context,arg1 *items.Item, options ...*items.UndeleteOptions) (res0 error) {
+func (set EndpointsSet) Undelete(arg0 context.Context, arg1 *items.Item, options ...*items.UndeleteOptions) (res0 error) {
 	request := UndeleteRequest{Item: arg1, Options: options}
 	_, res0 = set.UndeleteEndpoint(arg0, &request)
 	if res0 != nil {
diff --git a/pkg/items/transport/grpc/protobuf_type_converters.microgen.go b/pkg/items/transport/grpc/protobuf_type_converters.microgen.go
index 63182822f884b61f9e5d9e21f7d73ba1eadf0f69..212b1ecd2bfb27db8b49d0f965b484226265f189 100644
--- a/pkg/items/transport/grpc/protobuf_type_converters.microgen.go
+++ b/pkg/items/transport/grpc/protobuf_type_converters.microgen.go
@@ -318,7 +318,7 @@ func DeleteOptionsToProto(options []*service.DeleteOptions) (*pb.DeleteOptions,
 
 	return &pb.DeleteOptions{
 		UpdateAttrs: opts.UpdateAttrs,
-		Erase: opts.Erase,
+		Erase:       opts.Erase,
 	}, nil
 }
 
diff --git a/pkg/organizations/transport/exchanges.microgen.go b/pkg/organizations/transport/exchanges.microgen.go
index 7ae098e5b5b555cfe834c55e2fe73c79805fdbfd..480cc3e5b7b3d6ea68bfa2e9bea23f0156d6f3d0 100644
--- a/pkg/organizations/transport/exchanges.microgen.go
+++ b/pkg/organizations/transport/exchanges.microgen.go
@@ -36,7 +36,7 @@ type (
 
 	FindRequest struct {
 		Filter *organizations.Filter `json:"filter"`
-		Opts   *options.FindOptions `json:"opts"`
+		Opts   *options.FindOptions  `json:"opts"`
 	}
 	FindResponse struct {
 		Orgs  []*organizations.Organization `json:"orgs"`
diff --git a/pkg/setup/client.go b/pkg/setup/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..98fa6a0e7a0d9e7d7676b89b43e096121f1804fd
--- /dev/null
+++ b/pkg/setup/client.go
@@ -0,0 +1,188 @@
+package setup
+
+import (
+	"context"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"go.uber.org/zap"
+)
+
+var (
+	ErrCheckClients     = errors.New("clients check error")
+	ErrInstallClients   = errors.New("failed to install clients")
+	ErrUninstallClients = errors.New("failed to uninstall clients")
+)
+
+type ClientsOption func(c *ClientConfig)
+type UpdateClientFn func(s *Setup, exist, new *clients.Client) (*clients.Client, bool)
+type DeleteClientFn func(s *Setup, client *clients.Client) bool
+
+type ClientConfig struct {
+	client   *clients.Client
+	UpdateFn UpdateClientFn
+	DeleteFn DeleteClientFn
+}
+
+func NewClientConfig(client *clients.Client, opt ...ClientsOption) ClientConfig {
+	c := ClientConfig{client: client}
+
+	UpdateExistingClient()(&c)
+	DeleteClientIfRemove()(&c)
+
+	for _, o := range opt {
+		o(&c)
+	}
+
+	return c
+}
+
+func OverwriteClient() ClientsOption {
+	return func(c *ClientConfig) {
+		c.UpdateFn = func(s *Setup, old, new *clients.Client) (*clients.Client, bool) { return new, true }
+	}
+}
+
+func KeepExistingClient() ClientsOption {
+	return func(c *ClientConfig) {
+		c.UpdateFn = func(s *Setup, old, new *clients.Client) (*clients.Client, bool) { return old, false }
+	}
+}
+
+func DeleteClient() ClientsOption {
+	return func(c *ClientConfig) {
+		c.DeleteFn = func(s *Setup, client *clients.Client) bool { return true }
+	}
+}
+
+func DeleteClientIfRemove() ClientsOption {
+	return func(c *ClientConfig) {
+		c.DeleteFn = func(s *Setup, client *clients.Client) bool { return s.IsRemove() }
+	}
+}
+
+func UpdateExistingClient() ClientsOption {
+	return func(c *ClientConfig) {
+		c.UpdateFn = func(s *Setup, exist, client *clients.Client) (*clients.Client, bool) {
+			if exist.Name == "" {
+				exist.Name = client.Name
+			}
+
+			if exist.Description == "" {
+				exist.Description = client.Description
+			}
+
+			if exist.OAuth == nil {
+				exist.OAuth = client.OAuth
+			}
+
+			if exist.TLS == nil {
+				exist.TLS = client.TLS
+			}
+
+			if exist.APIKey == nil {
+				exist.APIKey = client.APIKey
+			}
+
+			exist.Disabled = client.Disabled
+			exist.RoleID = client.RoleID
+
+			return exist, true
+		}
+	}
+}
+
+func (s *Setup) InstallClients(ctx context.Context) error {
+	if len(s.Clients) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Install clients", zap.String("Space ID", s.SpaceID))
+
+	for _, c := range s.Clients {
+		err := s.InstallClient(ctx, c)
+		if err != nil {
+			s.logger.Error("Failed to install client", zap.String("Client ID", c.client.ID), zap.String("Client Name", c.client.Name), zap.Error(err))
+			return errors.WithDetailf(errors.Wrap(err, "failed to install client"), "Возникла ошибка при настройке клиента %s(%s)", c.client.Name, c.client.ID)
+		}
+	}
+	return nil
+}
+
+func (s *Setup) InstallClient(ctx context.Context, c ClientConfig) error {
+	client := c.client
+	client.SpaceID = s.SpaceID
+
+	if s.IsForce() {
+		s.content.Clients.Delete(ctx, s.SpaceID, c.client.ID)
+		_, err := s.content.Clients.Create(ctx, c.client)
+		return err
+	}
+
+	exist, err := s.content.Clients.Get(ctx, s.SpaceID, c.client.ID)
+	if err != nil {
+		if !strings.Contains(err.Error(), clients.ErrNotFound.Error()) {
+			return err
+		}
+
+		_, err = s.content.Clients.Create(ctx, c.client)
+		return err
+	}
+
+	if client, upd := c.UpdateFn(s, exist, c.client); upd {
+		return s.content.Clients.Update(ctx, client)
+	}
+
+	return nil
+}
+
+func (s *Setup) CheckClients(ctx context.Context) (err error) {
+	if len(s.Clients) == 0 {
+		return nil
+	}
+
+	var errs []error
+	s.logger.Debug("Check clients", zap.String("Space ID", s.SpaceID))
+	for _, c := range s.Clients {
+		err := s.CheckClient(ctx, c.client)
+		if err != nil {
+			errs = append(errs, errors.WithDetailf(err, "Не найден клиент %s(%s)", c.client.Name, c.client.ID))
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.WithErrors(ErrCheckClients, errs...)
+	}
+
+	return nil
+}
+
+func (s *Setup) CheckClient(ctx context.Context, client *clients.Client) error {
+	_, err := s.content.Clients.Get(ctx, s.SpaceID, client.ID)
+	return err
+}
+
+func (s *Setup) UninstallClients(ctx context.Context) error {
+	if len(s.Clients) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Uninstall clients", zap.String("Space ID", s.SpaceID))
+
+	for _, c := range s.Clients {
+		if err := s.UninstallClient(ctx, c); err != nil {
+			s.logger.Error("Failed to uninstall client", zap.String("Client ID", c.client.ID), zap.String("Client Name", c.client.Name), zap.Error(err))
+			return errors.WithDetailf(errors.Wrap(err, "failed to uninstall client"), "Возникла ошибка при удалении клиента %s(%s)", c.client.Name, c.client.ID)
+		}
+	}
+
+	return nil
+}
+
+func (s *Setup) UninstallClient(ctx context.Context, c ClientConfig) error {
+	if c.DeleteFn(s, c.client) {
+		return s.content.Clients.Delete(ctx, s.SpaceID, c.client.ID)
+	}
+	return nil
+}
diff --git a/pkg/setup/client_test.go b/pkg/setup/client_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..d345756a2b0b87e7ad10aebc1ee9ce0899df199c
--- /dev/null
+++ b/pkg/setup/client_test.go
@@ -0,0 +1,97 @@
+package setup
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	clientsMock "git.perx.ru/perxis/perxis-go/pkg/clients/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestSetup_InstallClients(t *testing.T) {
+
+	tests := []struct {
+		name        string
+		clients     []*clients.Client
+		clientsCall func(svc *clientsMock.Clients)
+		wantErr     func(t *testing.T, err error)
+	}{
+		{
+			name:        "Nil clients",
+			clients:     nil,
+			clientsCall: func(svc *clientsMock.Clients) {},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:    "Install one client success",
+			clients: []*clients.Client{{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}},
+			clientsCall: func(svc *clientsMock.Clients) {
+				svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}).Return(&clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}, nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:    "Install one client fails",
+			clients: []*clients.Client{{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}},
+			clientsCall: func(svc *clientsMock.Clients) {
+				svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}).Return(nil, errors.New("some error")).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.Error(t, err)
+				assert.EqualError(t, err, "failed to install client: some error")
+				assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при настройке клиента client(1)")
+			},
+		},
+		{
+			name:    "Install multiple clients success",
+			clients: []*clients.Client{{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}, {ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}},
+			clientsCall: func(svc *clientsMock.Clients) {
+				svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Get", mock.Anything, "sp", "2").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}).Return(&clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}, nil).Once()
+				svc.On("Create", mock.Anything, &clients.Client{ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}).Return(&clients.Client{ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}, nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:    "Install multiple clients fails",
+			clients: []*clients.Client{{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}, {ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}},
+			clientsCall: func(svc *clientsMock.Clients) {
+				svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Get", mock.Anything, "sp", "2").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &clients.Client{ID: "1", SpaceID: "sp", Name: "client", RoleID: "role-1"}).Return(nil, errors.New("some error")).Once()
+				svc.On("Create", mock.Anything, &clients.Client{ID: "2", SpaceID: "sp", Name: "client", RoleID: "role-2"}).Return(nil, errors.New("some error")).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.Error(t, err)
+				assert.EqualError(t, err, "failed to install client: some error")
+				assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при настройке клиента client(1)")
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &clientsMock.Clients{}
+			if tt.clientsCall != nil {
+				tt.clientsCall(c)
+			}
+
+			s := NewSetup(&content.Content{Clients: c}, "sp", "env", nil)
+			s.AddClients(tt.clients)
+			tt.wantErr(t, s.InstallClients(context.Background()))
+		})
+	}
+}
diff --git a/pkg/setup/collection.go b/pkg/setup/collection.go
new file mode 100644
index 0000000000000000000000000000000000000000..d61ba14d18f2d8bc93d2a535392b8b608ba6e2d0
--- /dev/null
+++ b/pkg/setup/collection.go
@@ -0,0 +1,215 @@
+package setup
+
+import (
+	"context"
+	"reflect"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"go.uber.org/zap"
+)
+
+var (
+	ErrCheckCollections     = errors.New("collections check error")
+	ErrInstallCollections   = errors.New("failed to install collections")
+	ErrUninstallCollections = errors.New("failed to uninstall collections")
+)
+
+type CollectionsOption func(c *CollectionConfig)
+type UpdateCollectionFn func(s *Setup, exist, new *collections.Collection) (coll *collections.Collection, upd bool, setSchema bool)
+type DeleteCollectionFn func(s *Setup, col *collections.Collection) bool
+
+type CollectionConfig struct {
+	collection *collections.Collection
+	UpdateFn   UpdateCollectionFn
+	DeleteFn   DeleteCollectionFn
+}
+
+func NewCollectionConfig(collection *collections.Collection, opt ...CollectionsOption) CollectionConfig {
+	c := CollectionConfig{collection: collection}
+
+	UpdateExistingCollection()(&c)
+	DeleteCollectionIfRemove()(&c)
+
+	for _, o := range opt {
+		o(&c)
+	}
+
+	return c
+}
+
+func OverwriteCollection() CollectionsOption {
+	return func(c *CollectionConfig) {
+		c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool) {
+			return new, true, true
+		}
+	}
+}
+
+func KeepExistingCollection() CollectionsOption {
+	return func(c *CollectionConfig) {
+		c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool) {
+			return old, false, false
+		}
+	}
+}
+
+func DeleteCollection() CollectionsOption {
+	return func(c *CollectionConfig) {
+		c.DeleteFn = func(s *Setup, collection *collections.Collection) bool { return true }
+	}
+}
+
+func DeleteCollectionIfRemove() CollectionsOption {
+	return func(c *CollectionConfig) {
+		c.DeleteFn = func(s *Setup, collection *collections.Collection) bool { return s.IsRemove() }
+	}
+}
+
+func UpdateExistingCollection() CollectionsOption {
+	return func(c *CollectionConfig) {
+		c.UpdateFn = func(s *Setup, exist, collection *collections.Collection) (*collections.Collection, bool, bool) {
+			if len(exist.Tags) > 0 {
+				collection.Tags = append(exist.Tags, collection.Tags...)
+			}
+
+			return collection, true, !collection.IsView() && !reflect.DeepEqual(exist.Schema, collection.Schema)
+		}
+	}
+}
+
+func (s *Setup) InstallCollections(ctx context.Context) (err error) {
+	if len(s.Collections) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Install collections", zap.Int("Collections", len(s.Collections)))
+
+	var migrate, setSchema bool
+
+	for _, c := range s.Collections {
+		setSchema, err = s.InstallCollection(ctx, c)
+		if err != nil {
+			s.logger.Error("Failed to install collection",
+				zap.String("Collection ID", c.collection.ID),
+				zap.String("Collection Name", c.collection.Name),
+				zap.Error(err),
+			)
+			return errors.WithDetailf(errors.Wrap(err, "failed to install collection"), "Возникла ошибка при настройке коллекции %s(%s)", c.collection.Name, c.collection.ID)
+		}
+		if setSchema {
+			migrate = true
+		}
+	}
+
+	if migrate {
+		if err = s.content.Environments.Migrate(ctx, s.SpaceID, s.EnvironmentID, &environments.MigrateOptions{Wait: true}); err != nil {
+			s.logger.Error(
+				"Failed to migrate environment",
+				zap.String("Space ID", s.SpaceID),
+				zap.String("Environment ID", s.EnvironmentID),
+				zap.Error(err),
+			)
+
+			return errors.WithErrors(ErrInstallCollections, err)
+		}
+	}
+
+	return nil
+}
+
+func (s *Setup) InstallCollection(ctx context.Context, c CollectionConfig) (setSchema bool, err error) {
+	collection := c.collection
+	collection.SpaceID, collection.EnvID = s.SpaceID, s.EnvironmentID
+
+	var exist *collections.Collection
+	// isForce - не удалять коллекцию, если она уже существует
+	exist, err = s.content.Collections.Get(ctx, collection.SpaceID, collection.EnvID, collection.ID)
+	if err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) {
+		return false, err
+	}
+
+	if exist == nil {
+		setSchema = !collection.IsView()
+		exist, err = s.content.Collections.Create(ctx, collection)
+		if err != nil {
+			return false, err
+		}
+	} else {
+		var upd bool
+		collection, upd, setSchema = c.UpdateFn(s, exist, c.collection)
+		if upd {
+			if err = s.content.Collections.Update(ctx, collection); err != nil {
+				return false, err
+			}
+		}
+	}
+
+	if setSchema {
+		err = s.content.Collections.SetSchema(ctx, collection.SpaceID, collection.EnvID, collection.ID, collection.Schema)
+		if err != nil {
+			return false, err
+		}
+	}
+
+	return setSchema, nil
+}
+
+func (s *Setup) CheckCollections(ctx context.Context) error {
+	if len(s.Collections) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Check collections", zap.Int("Collections", len(s.Collections)))
+
+	var errs []error
+	for _, c := range s.Collections {
+		if err := s.CheckCollection(ctx, c); err != nil {
+			errs = append(errs, errors.WithDetailf(err, "Не найдена коллекция %s(%s)", c.collection.ID, c.collection.Name))
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.WithErrors(ErrCheckCollections, errs...)
+	}
+
+	return nil
+}
+
+func (s *Setup) CheckCollection(ctx context.Context, c CollectionConfig) (err error) {
+	_, err = s.content.Collections.Get(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID)
+	return err
+}
+
+func (s *Setup) UninstallCollections(ctx context.Context) error {
+	if len(s.Collections) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Uninstall collections", zap.Int("Collections", len(s.Collections)))
+
+	for _, c := range s.Collections {
+		if err := s.UninstallCollection(ctx, c); err != nil {
+			s.logger.Error("Failed to uninstall collection",
+				zap.String("Collection ID", c.collection.ID),
+				zap.String("Collection Name", c.collection.Name),
+				zap.Error(err),
+			)
+			return errors.WithDetailf(errors.Wrap(err, "failed to uninstall collection"), "Возникла ошибка при удалении коллекции %s(%s)", c.collection.Name, c.collection.ID)
+		}
+	}
+
+	return nil
+}
+
+func (s *Setup) UninstallCollection(ctx context.Context, c CollectionConfig) error {
+	if c.DeleteFn(s, c.collection) {
+		if err := s.content.Collections.Delete(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID); err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) {
+			return err
+		}
+		s.removeItems(c.collection.ID) // после удаления коллекции нет смысла удалять ее элементы
+	}
+	return nil
+}
diff --git a/pkg/setup/collection_test.go b/pkg/setup/collection_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..433aa7397d450f8226a95af032a7cd177697fbed
--- /dev/null
+++ b/pkg/setup/collection_test.go
@@ -0,0 +1,112 @@
+package setup
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	mockscollections "git.perx.ru/perxis/perxis-go/pkg/collections/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"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/mock"
+)
+
+func TestSetup_InstallCollections(t *testing.T) {
+	tests := []struct {
+		name            string
+		collections     []*collections.Collection
+		collectionsCall func(svc *mockscollections.Collections)
+		envsCall        func(svc *envmocks.Environments)
+		wantErr         func(t *testing.T, err error)
+	}{
+		{
+			name:            "Nil collections",
+			collections:     nil,
+			collectionsCall: func(svc *mockscollections.Collections) {},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:        "Install one collection success",
+			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}},
+			collectionsCall: func(svc *mockscollections.Collections) {
+				svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(&collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, nil).Once()
+				svc.On("SetSchema", mock.Anything, "sp", "env", "1", schema.New("name", field.String())).Return(nil).Once()
+			},
+			envsCall: func(svc *envmocks.Environments) {
+				svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:        "Install one collection fails",
+			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}},
+			collectionsCall: func(svc *mockscollections.Collections) {
+				svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(nil, errors.New("some error")).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.Error(t, err)
+				assert.EqualError(t, err, "failed to install collection: some error")
+				assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при настройке коллекции space(1)")
+			},
+		},
+		{
+			name:        "Install multiple collections success",
+			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, {ID: "2", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}},
+			collectionsCall: func(svc *mockscollections.Collections) {
+				svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Get", mock.Anything, "sp", "env", "2").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(&collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, nil).Once()
+				svc.On("Create", mock.Anything, &collections.Collection{ID: "2", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(&collections.Collection{ID: "2", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, nil).Once()
+				svc.On("SetSchema", mock.Anything, "sp", "env", "1", schema.New("name", field.String())).Return(nil).Once()
+				svc.On("SetSchema", mock.Anything, "sp", "env", "2", schema.New("name", field.String())).Return(nil).Once()
+			},
+			envsCall: func(svc *envmocks.Environments) {
+				svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Twice()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:        "Install multiple collections fails",
+			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}, {ID: "2", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}},
+			collectionsCall: func(svc *mockscollections.Collections) {
+				svc.On("Get", mock.Anything, "sp", "env", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}).Return(nil, errors.New("some error")).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.Error(t, err)
+				assert.EqualError(t, err, "failed to install collection: some error")
+				assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при настройке коллекции space(1)")
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &mockscollections.Collections{}
+			e := &envmocks.Environments{}
+			if tt.collectionsCall != nil {
+				tt.collectionsCall(c)
+			}
+			if tt.envsCall != nil {
+				tt.envsCall(e)
+			}
+
+			s := NewSetup(&content.Content{Collections: c, Environments: e}, "sp", "env", nil)
+			s.AddCollections(tt.collections)
+			tt.wantErr(t, s.InstallCollections(context.Background()))
+		})
+	}
+}
diff --git a/pkg/setup/item.go b/pkg/setup/item.go
new file mode 100644
index 0000000000000000000000000000000000000000..d275d0ff38fa33e7c2a77010344b5f6117d2310d
--- /dev/null
+++ b/pkg/setup/item.go
@@ -0,0 +1,278 @@
+package setup
+
+import (
+	"context"
+	"reflect"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"go.uber.org/zap"
+)
+
+var (
+	ErrCheckItems     = errors.New("items check error")
+	ErrInstallItems   = errors.New("failed to install items")
+	ErrUninstallItems = errors.New("failed to uninstall items")
+	ErrItemsNotFound  = errors.New("item not found")
+)
+
+type ItemsOption func(c *ItemConfig)
+type UpdateItemFn func(s *Setup, exist, new *items.Item) (*items.Item, bool)
+type DeleteItemFn func(s *Setup, col *items.Item) bool
+
+type ItemConfig struct {
+	item     *items.Item
+	UpdateFn UpdateItemFn
+	DeleteFn DeleteItemFn
+}
+
+func NewItemConfig(item *items.Item, opt ...ItemsOption) ItemConfig {
+	c := ItemConfig{item: item}
+
+	KeepExistingItem()(&c)
+	DeleteItemIfRemove()(&c)
+
+	for _, o := range opt {
+		o(&c)
+	}
+
+	return c
+}
+
+func OverwriteItem() ItemsOption {
+	return func(c *ItemConfig) {
+		c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return new, true }
+	}
+}
+
+func OverwriteFields(fields ...string) ItemsOption {
+	return func(c *ItemConfig) {
+		c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) {
+
+			var changed bool
+			for _, field := range fields {
+				if items.IsSystemField(field) {
+					continue
+				}
+
+				newValue, err := new.Get(field)
+				if err != nil {
+					continue
+				}
+
+				oldValue, err := old.Get(field)
+				if err != nil || newValue != oldValue {
+					changed = true
+					if err = old.Set(field, newValue); err != nil {
+						return nil, false // не обновляем данные если не удалось установить значение
+					}
+				}
+			}
+
+			return old, changed
+		}
+	}
+}
+
+func KeepFields(fields ...string) ItemsOption {
+	return func(c *ItemConfig) {
+		c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) {
+
+			for _, field := range fields {
+				if items.IsSystemField(field) {
+					continue
+				}
+
+				oldValue, err := old.Get(field)
+				if err != nil {
+					continue
+				}
+
+				newValue, err := new.Get(field)
+				if err != nil || newValue != oldValue {
+					if err = new.Set(field, oldValue); err != nil {
+						return nil, false // не обновляем данные если не удалось установить значение
+					}
+				}
+			}
+
+			return new, !reflect.DeepEqual(old, new)
+		}
+	}
+}
+
+func KeepExistingItem() ItemsOption {
+	return func(c *ItemConfig) {
+		c.UpdateFn = func(s *Setup, old, new *items.Item) (*items.Item, bool) { return old, false }
+	}
+}
+
+func DeleteItem() ItemsOption {
+	return func(c *ItemConfig) {
+		c.DeleteFn = func(s *Setup, item *items.Item) bool { return true }
+	}
+}
+
+func DeleteItemIfRemove() ItemsOption {
+	return func(c *ItemConfig) {
+		c.DeleteFn = func(s *Setup, item *items.Item) bool { return s.IsRemove() }
+	}
+}
+
+func (s *Setup) InstallItems(ctx context.Context) error {
+	if len(s.Items) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Install items", zap.Int("Items", len(s.Items)))
+
+	for col, itms := range s.groupByCollection() {
+		exists, err := s.getExisting(ctx, col, itms)
+		if err != nil {
+			return err
+		}
+		for _, c := range itms {
+			if err := s.InstallItem(ctx, exists, c); err != nil {
+				s.logger.Error("Failed to install item",
+					zap.String("ID", c.item.ID),
+					zap.String("Collection", c.item.CollectionID),
+					zap.Error(err),
+				)
+				return errors.WithDetailf(errors.Wrap(err, "failed to install item"), "Возникла ошибка при добавлении элемента %s(%s)", c.item.ID, c.item.CollectionID)
+			}
+		}
+	}
+
+	return nil
+}
+
+func (s *Setup) InstallItem(ctx context.Context, exists map[string]*items.Item, c ItemConfig) error {
+	item := c.item
+	item.SpaceID, item.EnvID = s.SpaceID, s.EnvironmentID
+
+	exist, ok := exists[item.ID]
+	if !ok {
+		return items.CreateAndPublishItem(ctx, s.content.Items, item)
+	}
+
+	if item, changed := c.UpdateFn(s, exist, c.item); changed {
+		return items.UpdateAndPublishItem(ctx, s.content.Items, item)
+	}
+
+	return nil
+}
+
+func (s *Setup) UninstallItems(ctx context.Context) error {
+	if len(s.Items) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Uninstall items", zap.Int("Items", len(s.Items)))
+
+	for _, c := range s.Items {
+		if err := s.UninstallItem(ctx, c); err != nil {
+			s.logger.Error("Failed to uninstall item",
+				zap.String("Item", c.item.ID),
+				zap.String("Item", c.item.CollectionID),
+				zap.Error(err),
+			)
+			return errors.WithDetailf(errors.Wrap(err, "failed to uninstall item"), "Возникла ошибка при удалении элемента %s(%s)", c.item.ID, c.item.CollectionID)
+		}
+	}
+
+	return nil
+}
+
+func (s *Setup) UninstallItem(ctx context.Context, c ItemConfig) error {
+	if c.DeleteFn(s, c.item) {
+		err := s.content.Items.Delete(ctx, &items.Item{SpaceID: s.SpaceID, EnvID: s.EnvironmentID, CollectionID: c.item.CollectionID, ID: c.item.ID})
+		if err != nil && !strings.Contains(err.Error(), items.ErrNotFound.Error()) {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *Setup) CheckItems(ctx context.Context) error {
+	if len(s.Items) == 0 {
+		return nil
+	}
+
+	var errs []error
+	s.logger.Debug("Check items", zap.Int("Items", len(s.Items)))
+
+	for col, itms := range s.groupByCollection() {
+		exists, err := s.getExisting(ctx, col, itms)
+		if err != nil {
+			return err
+		}
+
+		for _, c := range itms {
+			if _, ok := exists[c.item.ID]; !ok {
+				errs = append(errs, errors.WithDetailf(errors.New("not found"), "Не найден элемент %s(%s)", c.item.ID, c.item.CollectionID))
+			}
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.WithErrors(ErrCheckItems, errs...)
+	}
+
+	return nil
+}
+
+func (s *Setup) removeItems(collID string) {
+	itms := make([]ItemConfig, 0, len(s.Items))
+	for _, i := range s.Items {
+		if i.item.CollectionID != collID {
+			itms = append(itms, i)
+		}
+	}
+	s.Items = itms
+}
+
+func (s *Setup) groupByCollection() map[string][]ItemConfig {
+	itemsByColl := map[string][]ItemConfig{}
+	for _, i := range s.Items {
+		cfg, ok := itemsByColl[i.item.CollectionID]
+		if !ok {
+			itemsByColl[i.item.CollectionID] = []ItemConfig{i}
+			continue
+		}
+		itemsByColl[i.item.CollectionID] = append(cfg, i)
+	}
+	return itemsByColl
+}
+
+func (s *Setup) getExisting(ctx context.Context, collID string, configs []ItemConfig) (map[string]*items.Item, error) {
+	itms, _, err := s.content.Items.Find(
+		ctx,
+		s.SpaceID,
+		s.EnvironmentID,
+		collID,
+		&items.Filter{ID: getItemIds(configs)},
+		&items.FindOptions{Regular: true, Hidden: true, Templates: true},
+	)
+	if err != nil {
+		s.logger.Error("Failed to find existing items",
+			zap.String("Collection", collID),
+			zap.Error(err),
+		)
+		return nil, errors.WithDetailf(errors.Wrap(err, "failed to find existing items"), "Возникла ошибка при поиске элементов в коллекции %s", collID)
+	}
+
+	exists := make(map[string]*items.Item, len(itms))
+	for _, itm := range itms {
+		exists[itm.ID] = itm
+	}
+	return exists, nil
+}
+
+func getItemIds(items []ItemConfig) []string {
+	var ids []string
+	for _, i := range items {
+		ids = append(ids, i.item.ID)
+	}
+	return ids
+}
diff --git a/pkg/setup/item_test.go b/pkg/setup/item_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..920292f501cf9e469150621b3894bcad41326aa8
--- /dev/null
+++ b/pkg/setup/item_test.go
@@ -0,0 +1,279 @@
+package setup
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	itemsMock "git.perx.ru/perxis/perxis-go/pkg/items/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestItem_OverwriteFields(t *testing.T) {
+	tests := []struct {
+		name    string
+		fields  []string
+		new     *items.Item
+		old     *items.Item
+		want    *items.Item
+		changed bool
+	}{
+		{
+			name:    "Empty",
+			fields:  []string{},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			changed: false,
+		},
+		{
+			name:    "Not found",
+			fields:  []string{"notfound"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			changed: false,
+		},
+		{
+			name:    "Equal value",
+			fields:  []string{"key"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			changed: false,
+		},
+		{
+			name:    "Not Equal value #1",
+			fields:  []string{"key"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key2": "value2"}},
+			changed: true,
+		},
+		{
+			name:    "Not Equal value #2",
+			fields:  []string{"key", "key2"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key2": "value2"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key3": "value3"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key2": "value2", "key3": "value3"}},
+			changed: true,
+		},
+		{
+			name:    "Equal nested",
+			fields:  []string{"hoop.exclude"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}, "key": "value"}},
+			changed: false,
+		},
+		{
+			name:    "Not Equal nested",
+			fields:  []string{"hoop.exclude"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": false}, "key": "value"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}, "key": "value"}},
+			changed: true,
+		},
+		{
+			name:    "System",
+			fields:  []string{"id"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"id": "value"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"id": "value"}},
+			changed: false,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			c := ItemConfig{item: test.new}
+			OverwriteFields(test.fields...)(&c)
+
+			got, changed := c.UpdateFn(nil, test.old, test.new)
+			require.Equal(t, test.changed, changed)
+			if !test.changed {
+				return
+			}
+			assert.Equal(t, test.want, got)
+
+		})
+	}
+}
+
+func TestItem_KeepFields(t *testing.T) {
+	tests := []struct {
+		name    string
+		fields  []string
+		new     *items.Item
+		old     *items.Item
+		want    *items.Item
+		changed bool
+	}{
+		{
+			name:    "Empty",
+			fields:  []string{},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "0"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			changed: true,
+		},
+		{
+			name:    "Not found",
+			fields:  []string{"notfound"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "0"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			changed: true,
+		},
+		{
+			name:    "Equal value",
+			fields:  []string{"key"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}},
+			changed: true,
+		},
+		{
+			name:    "Not Equal value #1",
+			fields:  []string{"key"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key3": "value3"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key3": "value3"}},
+			changed: true,
+		},
+		{
+			name:    "Not Equal value #2",
+			fields:  []string{"key", "key2"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value1", "key3": "value3"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"key": "value", "key2": "value2", "key3": "value3"}},
+			changed: true,
+		},
+		{
+			name:    "Equal nested",
+			fields:  []string{"hoop.exclude"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}, "key": "value"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}},
+			changed: true,
+		},
+		{
+			name:    "Not Equal nested",
+			fields:  []string{"hoop.exclude"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": true}}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": false}, "key": "value"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"hoop": map[string]interface{}{"exclude": false}}},
+			changed: true,
+		},
+		{
+			name:    "System",
+			fields:  []string{"id"},
+			new:     &items.Item{ID: "id", Data: map[string]interface{}{"id": "value1"}},
+			old:     &items.Item{ID: "id", Data: map[string]interface{}{"id": "value"}},
+			want:    &items.Item{ID: "id", Data: map[string]interface{}{"id": "value1"}},
+			changed: true,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			c := ItemConfig{item: test.new}
+			KeepFields(test.fields...)(&c)
+
+			got, changed := c.UpdateFn(nil, test.old, test.new)
+			require.Equal(t, test.changed, changed)
+			if !test.changed {
+				return
+			}
+			assert.Equal(t, test.want, got)
+
+		})
+	}
+}
+
+func TestSetup_InstallItems(t *testing.T) {
+
+	tests := []struct {
+		name      string
+		items     []*items.Item
+		itemsCall func(svc *itemsMock.Items)
+		wantErr   func(t *testing.T, err error)
+	}{
+		{
+			name:      "Nil clients",
+			items:     nil,
+			itemsCall: func(svc *itemsMock.Items) {},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:  "Install one item success",
+			items: []*items.Item{{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}},
+			itemsCall: func(svc *itemsMock.Items) {
+				svc.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once()
+				svc.On("Create", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(&items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, nil).Once()
+				svc.On("Publish", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:  "Install one item fails",
+			items: []*items.Item{{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}},
+			itemsCall: func(svc *itemsMock.Items) {
+				svc.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once()
+				svc.On("Create", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil, errors.New("some error")).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.Error(t, err)
+				assert.EqualError(t, err, "failed to install item: create item: some error")
+				assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при добавлении элемента 1(coll)")
+			},
+		},
+		{
+			name: "Install multiple items success",
+			items: []*items.Item{
+				{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}},
+				{ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}},
+			},
+			itemsCall: func(svc *itemsMock.Items) {
+				svc.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once()
+				svc.On("Create", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(&items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, nil).Once()
+				svc.On("Create", mock.Anything, &items.Item{ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(&items.Item{ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}, nil).Once()
+				svc.On("Publish", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil).Once()
+				svc.On("Publish", mock.Anything, &items.Item{ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name: "Install multiple items fails",
+			items: []*items.Item{
+				{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}},
+				{ID: "2", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}},
+			},
+			itemsCall: func(svc *itemsMock.Items) {
+				svc.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once()
+				svc.On("Create", mock.Anything, &items.Item{ID: "1", SpaceID: "sp", EnvID: "env", CollectionID: "coll", Data: map[string]interface{}{"text": "test"}}).Return(nil, errors.New("some error")).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.Error(t, err)
+				assert.EqualError(t, err, "failed to install item: create item: some error")
+				assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при добавлении элемента 1(coll)")
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			i := &itemsMock.Items{}
+			if tt.itemsCall != nil {
+				tt.itemsCall(i)
+			}
+
+			s := NewSetup(&content.Content{Items: i}, "sp", "env", nil)
+			s.AddItems(tt.items)
+			tt.wantErr(t, s.InstallItems(context.Background()))
+		})
+	}
+}
diff --git a/pkg/setup/role.go b/pkg/setup/role.go
new file mode 100644
index 0000000000000000000000000000000000000000..ac6e641a97c6160cf29c5353f0176ef110a9bab3
--- /dev/null
+++ b/pkg/setup/role.go
@@ -0,0 +1,201 @@
+package setup
+
+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/permission"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"go.uber.org/zap"
+)
+
+var (
+	ErrCheckRoles     = errors.New("role check error")
+	ErrInstallRoles   = errors.New("failed to install role")
+	ErrUninstallRoles = errors.New("failed to uninstall role")
+)
+
+type RolesOption func(c *RoleConfig)
+type UpdateRoleFn func(s *Setup, exist, new *roles.Role) (*roles.Role, bool)
+type DeleteRoleFn func(s *Setup, role *roles.Role) bool
+
+type RoleConfig struct {
+	role     *roles.Role
+	UpdateFn UpdateRoleFn
+	DeleteFn DeleteRoleFn
+}
+
+func NewRoleConfig(role *roles.Role, opt ...RolesOption) RoleConfig {
+	c := RoleConfig{role: role}
+
+	UpdateExistingRole()(&c)
+	DeleteRoleIfRemove()(&c)
+
+	for _, o := range opt {
+		o(&c)
+	}
+	return c
+}
+
+func OverwriteRole() RolesOption {
+	return func(c *RoleConfig) {
+		c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) { return new, true }
+	}
+}
+
+//func OverwriteRoleIfChanged() RolesOption {
+//	return func(c *RoleConfig) {
+//		c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) {
+//			changed := old.Description != new.Description || old.AllowManagement != new.AllowManagement ||
+//				!util.ElementsMatch(old.Environments, new.Environments)
+//			return new, changed
+//		}
+//	}
+//}
+
+func KeepExistingRole() RolesOption {
+	return func(c *RoleConfig) {
+		c.UpdateFn = func(s *Setup, old, new *roles.Role) (*roles.Role, bool) { return old, false }
+	}
+}
+
+func DeleteRole() RolesOption {
+	return func(c *RoleConfig) {
+		c.DeleteFn = func(s *Setup, role *roles.Role) bool { return true }
+	}
+}
+
+func DeleteRoleIfRemove() RolesOption {
+	return func(c *RoleConfig) {
+		c.DeleteFn = func(s *Setup, role *roles.Role) bool { return s.IsRemove() }
+	}
+}
+
+func UpdateExistingRole() RolesOption {
+	return func(c *RoleConfig) {
+		c.UpdateFn = func(s *Setup, exist, new *roles.Role) (*roles.Role, bool) {
+
+			// если передан флаг force, то обновляем все поля роли на переданные
+			if s.IsForce() {
+				return new, true
+			}
+
+			if exist.Description == "" {
+				exist.Description = new.Description
+			}
+
+			if len(exist.Environments) == 0 {
+				exist.Environments = new.Environments
+			}
+
+			if !data.Contains(s.EnvironmentID, exist.Environments) {
+				exist.Environments = append(exist.Environments, s.EnvironmentID)
+			}
+
+			exist.Rules = permission.MergeRules(exist.Rules, new.Rules)
+
+			if !exist.AllowManagement {
+				exist.AllowManagement = new.AllowManagement
+			}
+
+			return exist, true
+		}
+	}
+}
+
+func (s *Setup) InstallRoles(ctx context.Context) error {
+	if len(s.Roles) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Install role", zap.String("Space ID", s.SpaceID), zap.Int("Roles", len(s.Roles)))
+
+	for _, c := range s.Roles {
+		if err := s.InstallRole(ctx, c); err != nil {
+			s.logger.Error("Failed to install role", zap.String("Role ID", c.role.ID), zap.Error(err))
+			return errors.Wrap(errors.WithDetailf(err, "Возникла ошибка при настройке роли %s(%s)", c.role.ID, c.role.Description), "failed to install role")
+		}
+	}
+
+	return nil
+}
+
+func (s *Setup) InstallRole(ctx context.Context, c RoleConfig) error {
+	role := c.role
+	role.SpaceID = s.SpaceID
+
+	if !data.Contains(s.EnvironmentID, c.role.Environments) {
+		role.Environments = append(role.Environments, s.EnvironmentID)
+	}
+
+	exist, err := s.content.Roles.Get(ctx, s.SpaceID, role.ID)
+	if err != nil {
+		if !strings.Contains(err.Error(), roles.ErrNotFound.Error()) {
+			return err
+		}
+
+		_, err = s.content.Roles.Create(ctx, role)
+		return err
+	}
+
+	if r, upd := c.UpdateFn(s, exist, role); upd {
+		return s.content.Roles.Update(ctx, r)
+	}
+
+	return nil
+}
+
+func (s *Setup) UninstallRoles(ctx context.Context) error {
+	if len(s.Roles) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Uninstall role", zap.String("Space ID", s.SpaceID), zap.Int("Roles", len(s.Roles)))
+
+	for _, c := range s.Roles {
+		if err := s.UninstallRole(ctx, c); err != nil {
+			s.logger.Error("Failed to uninstall role", zap.String("Role ID", c.role.ID), zap.Error(err))
+			return errors.WithDetailf(errors.Wrap(err, "failed to uninstall role"), "Возникла ошибка при удалении роли %s(%s)", c.role.ID, c.role.Description)
+		}
+	}
+
+	return nil
+}
+
+func (s *Setup) UninstallRole(ctx context.Context, c RoleConfig) error {
+	if c.DeleteFn(s, c.role) {
+		err := s.content.Roles.Delete(ctx, s.SpaceID, c.role.ID)
+		if err != nil && !strings.Contains(err.Error(), roles.ErrNotFound.Error()) {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *Setup) CheckRoles(ctx context.Context) error {
+	if len(s.Roles) == 0 {
+		return nil
+	}
+
+	s.logger.Debug("Check role", zap.String("Space ID", s.SpaceID))
+
+	var errs []error
+	for _, c := range s.Roles {
+		if err := s.CheckRole(ctx, c.role); err != nil {
+			errs = append(errs, errors.WithDetailf(err, "Не найдена роль %s(%s)", c.role.ID, c.role.Description))
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.WithErrors(ErrCheckRoles, errs...)
+	}
+
+	return nil
+}
+
+func (s *Setup) CheckRole(ctx context.Context, role *roles.Role) error {
+	_, err := s.content.Roles.Get(ctx, s.SpaceID, role.ID)
+	return err
+}
diff --git a/pkg/setup/role_test.go b/pkg/setup/role_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..1c218270b5b50bf2bc8c6cd27364a65cd2d9b60b
--- /dev/null
+++ b/pkg/setup/role_test.go
@@ -0,0 +1,98 @@
+package setup
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	rolesMock "git.perx.ru/perxis/perxis-go/pkg/roles/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestSetup_InstallRoles(t *testing.T) {
+
+	tests := []struct {
+		name      string
+		roles     []*roles.Role
+		rolesCall func(svc *rolesMock.Roles)
+		wantErr   func(t *testing.T, err error)
+	}{
+		{
+			name:      "Nil role",
+			roles:     nil,
+			rolesCall: func(svc *rolesMock.Roles) {},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:  "Install one role success",
+			roles: []*roles.Role{{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}},
+			rolesCall: func(svc *rolesMock.Roles) {
+				svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(&roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}, nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:  "Install one role fails",
+			roles: []*roles.Role{{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}},
+			rolesCall: func(svc *rolesMock.Roles) {
+				svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("some error")).Once()
+				svc.On("Create", mock.Anything, &roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(nil, errors.New("failed to install")).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.Error(t, err)
+				assert.EqualError(t, err, "failed to install role: some error")
+				assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при настройке роли 1(test)")
+			},
+		},
+		{
+			name: "Install multiple roles success",
+			roles: []*roles.Role{
+				{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"},
+				{ID: "2", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}},
+			rolesCall: func(svc *rolesMock.Roles) {
+				svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("not found")).Once()
+				svc.On("Get", mock.Anything, "sp", "2").Return(nil, errors.New("not found")).Once()
+				svc.On("Create", mock.Anything, &roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(&roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}, nil).Once()
+				svc.On("Create", mock.Anything, &roles.Role{ID: "2", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(&roles.Role{ID: "2", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}, nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:  "Install multiple roles fails",
+			roles: []*roles.Role{{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}, {ID: "2", SpaceID: "sp", Environments: []string{"env"}}},
+			rolesCall: func(svc *rolesMock.Roles) {
+				svc.On("Get", mock.Anything, "sp", "1").Return(nil, errors.New("some error")).Once()
+				svc.On("Get", mock.Anything, "sp", "2").Return(nil, errors.New("some error")).Once()
+				svc.On("Create", mock.Anything, &roles.Role{ID: "1", SpaceID: "sp", Environments: []string{"env"}, Description: "test"}).Return(nil, errors.New("failed to install")).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.Error(t, err)
+				assert.EqualError(t, err, "failed to install role: some error")
+				assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при настройке роли 1(test)")
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &rolesMock.Roles{}
+			if tt.rolesCall != nil {
+				tt.rolesCall(r)
+			}
+
+			s := NewSetup(&content.Content{Roles: r}, "sp", "env", nil)
+			s.AddRoles(tt.roles)
+			tt.wantErr(t, s.InstallRoles(context.Background()))
+		})
+	}
+}
diff --git a/pkg/setup/setup.go b/pkg/setup/setup.go
new file mode 100644
index 0000000000000000000000000000000000000000..6360eadee88a9cc2345e140f7acf5829e0fc9f26
--- /dev/null
+++ b/pkg/setup/setup.go
@@ -0,0 +1,188 @@
+package setup
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"go.uber.org/zap"
+)
+
+// Setup реализует процесс настройки пространства. Указав необходимые требования к конфигурации пространства можно
+// выполнить процесс установки, проверки и удаления требований.
+type Setup struct {
+	SpaceID       string
+	EnvironmentID string
+
+	Roles       []RoleConfig
+	Clients     []ClientConfig
+	Collections []CollectionConfig
+	Items       []ItemConfig
+
+	content *content.Content
+
+	force  bool
+	remove bool
+
+	errors []error
+	logger *zap.Logger
+}
+
+func NewSetup(content *content.Content, spaceID, environmentID string, logger *zap.Logger) *Setup {
+	//logger = logger.With(zap.String("Space", spaceID), zap.String("Environment", environmentID))
+
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	return &Setup{
+		SpaceID:       spaceID,
+		EnvironmentID: environmentID,
+		content:       content,
+		logger:        logger,
+	}
+}
+
+func (s *Setup) WithForce(force bool) *Setup {
+	setup := *s
+	setup.force = force
+	return &setup
+}
+
+func (s *Setup) IsForce() bool {
+	return s.force
+}
+
+func (s *Setup) WithRemove(remove bool) *Setup {
+	setup := *s
+	setup.remove = remove
+	return &setup
+}
+
+func (s *Setup) IsRemove() bool {
+	return s.remove
+}
+
+func (s *Setup) HasErrors() bool {
+	return len(s.errors) > 0
+}
+
+func (s *Setup) AddError(err error) {
+	s.errors = append(s.errors, err)
+}
+
+// AddRoles добавляет требования к настройке ролей в пространстве
+func (s *Setup) AddRoles(roles []*roles.Role, opt ...RolesOption) *Setup {
+	for _, role := range roles {
+		s.AddRole(role, opt...)
+	}
+	return s
+}
+
+func (s *Setup) AddRole(role *roles.Role, opt ...RolesOption) *Setup {
+	s.Roles = append(s.Roles, NewRoleConfig(role, opt...))
+	return s
+}
+
+// AddClients добавляет требования к настройке приложений в пространстве
+func (s *Setup) AddClients(clients []*clients.Client, opt ...ClientsOption) *Setup {
+	for _, client := range clients {
+		s.AddClient(client, opt...)
+	}
+	return s
+}
+
+func (s *Setup) AddClient(client *clients.Client, opt ...ClientsOption) *Setup {
+	s.Clients = append(s.Clients, NewClientConfig(client, opt...))
+	return s
+}
+
+// AddCollections добавляет требования к настройке коллекций в пространстве
+func (s *Setup) AddCollections(collections []*collections.Collection, opt ...CollectionsOption) *Setup {
+	for _, col := range collections {
+		s.AddCollection(col, opt...)
+	}
+	return s
+}
+
+func (s *Setup) AddCollection(collection *collections.Collection, opt ...CollectionsOption) *Setup {
+	s.Collections = append(s.Collections, NewCollectionConfig(collection, opt...))
+	return s
+}
+
+// AddItems добавляет требования к настройке элементов в пространстве
+func (s *Setup) AddItems(items []*items.Item, opt ...ItemsOption) *Setup {
+	for _, item := range items {
+		s.AddItem(item, opt...)
+	}
+	return s
+}
+
+func (s *Setup) AddItem(item *items.Item, opt ...ItemsOption) *Setup {
+	s.Items = append(s.Items, NewItemConfig(item, opt...))
+	return s
+}
+
+// Install выполняет установку необходимых требований
+func (s *Setup) Install(ctx context.Context) error {
+	s.logger = s.logger.With(zap.String("Space", s.SpaceID), zap.String("Environment", s.EnvironmentID))
+
+	if err := s.InstallRoles(ctx); err != nil {
+		return err
+	}
+	if err := s.InstallClients(ctx); err != nil {
+		return err
+	}
+	if err := s.InstallCollections(ctx); err != nil {
+		return err
+	}
+	if err := s.InstallItems(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Check выполняет проверку требований
+func (s *Setup) Check(ctx context.Context) error {
+	s.logger = s.logger.With(zap.String("Space", s.SpaceID), zap.String("Environment", s.EnvironmentID))
+
+	if err := s.CheckRoles(ctx); err != nil {
+		return err
+	}
+	if err := s.CheckClients(ctx); err != nil {
+		return err
+	}
+	if err := s.CheckCollections(ctx); err != nil {
+		return err
+	}
+	if err := s.CheckItems(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Uninstall выполняет удаление установленных раннее требований
+func (s *Setup) Uninstall(ctx context.Context) error {
+	s.logger = s.logger.With(zap.String("Space", s.SpaceID), zap.String("Environment", s.EnvironmentID))
+
+	// В случае если необходимо удалить данные удаляем все что создано при установке расширения
+	if err := s.UninstallClients(ctx); err != nil {
+		return err
+	}
+	if err := s.UninstallRoles(ctx); err != nil {
+		return err
+	}
+	if err := s.UninstallCollections(ctx); err != nil {
+		return err
+	}
+	if err := s.UninstallItems(ctx); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/setup/setup_test.go b/pkg/setup/setup_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..979e29053bdb5316495a409aedd079afe34cb711
--- /dev/null
+++ b/pkg/setup/setup_test.go
@@ -0,0 +1,1219 @@
+package setup
+
+import (
+	"context"
+	"testing"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	clientsMock "git.perx.ru/perxis/perxis-go/pkg/clients/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	collectionMock "git.perx.ru/perxis/perxis-go/pkg/collections/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/data"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	environmentMock "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/extension"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	itemsMock "git.perx.ru/perxis/perxis-go/pkg/items/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	rolesMock "git.perx.ru/perxis/perxis-go/pkg/roles/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+	"go.uber.org/zap/zaptest"
+)
+
+const (
+	spaceID = "sp"
+	envID   = "env"
+)
+
+func getCollections() []*collections.Collection {
+	return []*collections.Collection{
+		{
+			ID:      "coll1",
+			SpaceID: spaceID,
+			EnvID:   envID,
+			Schema:  schema.New(),
+			Name:    "Коллекция",
+		},
+	}
+}
+
+func getRoles() []*roles.Role {
+	return []*roles.Role{
+		{
+			ID:      "role",
+			SpaceID: spaceID,
+		},
+	}
+}
+
+func getClients() []*clients.Client {
+	return []*clients.Client{
+		{
+			ID:      "client",
+			SpaceID: spaceID,
+			RoleID:  "role",
+		},
+	}
+}
+
+func getActions() []*items.Item {
+	return []*items.Item{
+		{
+			ID:           "act",
+			SpaceID:      spaceID,
+			EnvID:        envID,
+			CollectionID: extension.ActionsCollectionID,
+			Data: map[string]interface{}{
+				"action":    "act",
+				"name":      "Action",
+				"extension": "ext",
+			},
+		},
+	}
+}
+
+func newSetup(content *content.Content, t *testing.T) *Setup {
+	logger := zaptest.NewLogger(t, zaptest.WrapOptions())
+
+	setup := NewSetup(content, spaceID, envID, logger)
+	setup.AddCollections(getCollections())
+	setup.AddRoles(getRoles())
+	setup.AddClients(getClients())
+	setup.AddItems(getActions(), OverwriteItem())
+
+	return setup
+}
+
+func TestSetupInstall(t *testing.T) {
+	t.Run("Success, nothing to install", func(t *testing.T) {
+		logger := zaptest.NewLogger(t, zaptest.WrapOptions())
+
+		setup := NewSetup(nil, spaceID, envID, logger)
+
+		err := setup.Install(context.Background())
+
+		require.NoError(t, err)
+	})
+
+	t.Run("Success, no force", func(t *testing.T) {
+		envMocks := &environmentMock.Environments{}
+		envMocks.On("Migrate", mock.Anything, spaceID, envID, &environments.MigrateOptions{Wait: true}).
+			Return(nil).
+			Once()
+
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(nil, collections.ErrNotFound).
+				Once()
+
+			collsMock.On("Create", mock.Anything, collection).
+				Return(collection, nil).
+				Once()
+
+			collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema).
+				Return(nil).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(nil, roles.ErrNotFound).
+				Once()
+
+			rMock.On("Create", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				create := args[1].(*roles.Role)
+				require.True(t, data.Contains(envID, create.Environments))
+			}).Return(role, nil).Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(nil, clients.ErrNotFound).
+				Once()
+
+			clMock.On("Create", mock.Anything, client).
+				Return(client, nil).
+				Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		itmMock.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+			Return(nil, 0, nil).
+			Once()
+		for _, act := range getActions() {
+			itmMock.On("Create", mock.Anything, act).
+				Return(act, nil).
+				Once()
+			itmMock.On("Publish", mock.Anything, act).
+				Return(nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections:  collsMock,
+			Clients:      clMock,
+			Roles:        rMock,
+			Items:        itmMock,
+			Environments: envMocks,
+		}, t)
+
+		err := setup.Install(context.Background())
+
+		require.NoError(t, err)
+		require.False(t, setup.HasErrors())
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+		itmMock.AssertExpectations(t)
+		envMocks.AssertExpectations(t)
+	})
+
+	t.Run("Success, update existing records", func(t *testing.T) {
+		envMocks := &environmentMock.Environments{}
+
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(collection, nil).
+				Once()
+
+			collsMock.On("Update", mock.Anything, collection).
+				Return(nil).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(role, nil).
+				Once()
+
+			rMock.On("Update", mock.Anything, role).
+				Return(nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(client, nil).
+				Once()
+
+			clMock.On("Update", mock.Anything, client).
+				Return(nil).
+				Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		itms := getActions()
+		itmMock.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+			Return(itms, 0, nil).
+			Once()
+		itmMock.On("Update", mock.Anything, mock.Anything).
+			Return(nil).
+			Times(len(itms))
+		itmMock.On("Publish", mock.Anything, mock.Anything).
+			Return(nil).
+			Times(len(itms))
+
+		setup := newSetup(&content.Content{
+			Collections:  collsMock,
+			Clients:      clMock,
+			Roles:        rMock,
+			Items:        itmMock,
+			Environments: envMocks,
+		}, t)
+
+		err := setup.Install(context.Background())
+
+		require.NoError(t, err)
+		require.False(t, setup.HasErrors())
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+		itmMock.AssertExpectations(t)
+		envMocks.AssertExpectations(t)
+	})
+
+	t.Run("Success, with force", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(collection, nil).
+				Once()
+
+			collsMock.On("Update", mock.Anything, collection).
+				Return(nil).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).Return(role, nil).Once()
+			rMock.On("Update", mock.Anything, &roles.Role{ID: "role", SpaceID: "sp", Environments: []string{"env"}}).Return(nil).Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once()
+
+			clMock.On("Create", mock.Anything, client).
+				Return(client, nil).
+				Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		itmMock.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+			Return(nil, 0, nil).
+			Once()
+		for _, act := range getActions() {
+			itmMock.On("Create", mock.Anything, act).
+				Return(act, nil).
+				Once()
+			itmMock.On("Publish", mock.Anything, act).
+				Return(nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections: collsMock,
+			Clients:     clMock,
+			Roles:       rMock,
+			Items:       itmMock,
+		}, t)
+		setup = setup.WithForce(true)
+
+		err := setup.Install(context.Background())
+
+		require.NoError(t, err)
+		require.False(t, setup.HasErrors())
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+		itmMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't install role, storage returns error", func(t *testing.T) {
+		rMock := &rolesMock.Roles{}
+		rMock.On("Get", mock.Anything, spaceID, mock.Anything).
+			Return(nil, errors.New("can't get role")).
+			Once()
+
+		setup := newSetup(&content.Content{
+			Roles: rMock,
+		}, t)
+
+		err := setup.Install(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to install role")
+
+		rMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't install client, storage returns error", func(t *testing.T) {
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(nil, roles.ErrNotFound).
+				Once()
+
+			rMock.On("Create", mock.Anything, mock.Anything).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(nil, errors.New("can't get client")).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Clients: clMock,
+			Roles:   rMock,
+		}, t)
+
+		err := setup.Install(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to install client")
+
+		rMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't get collection, storage returns error", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(nil, errors.New("can't get collection")).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(nil, roles.ErrNotFound).
+				Once()
+
+			rMock.On("Create", mock.Anything, mock.Anything).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(nil, clients.ErrNotFound).
+				Once()
+
+			clMock.On("Create", mock.Anything, client).
+				Return(client, nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections: collsMock,
+			Clients:     clMock,
+			Roles:       rMock,
+		}, t)
+
+		err := setup.Install(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to install collection")
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't create collection, storage returns error", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(nil, collections.ErrNotFound).
+				Once()
+
+			collsMock.On("Create", mock.Anything, collection).
+				Return(nil, errors.New("can't create collection")).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(nil, roles.ErrNotFound).
+				Once()
+
+			rMock.On("Create", mock.Anything, mock.Anything).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(nil, clients.ErrNotFound).
+				Once()
+
+			clMock.On("Create", mock.Anything, client).
+				Return(client, nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections: collsMock,
+			Clients:     clMock,
+			Roles:       rMock,
+		}, t)
+
+		err := setup.Install(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to install collection")
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't update collection, storage returns error", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(collection, nil).
+				Once()
+
+			collsMock.On("Update", mock.Anything, collection).
+				Return(errors.New("can't update collection")).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(nil, roles.ErrNotFound).
+				Once()
+
+			rMock.On("Create", mock.Anything, mock.Anything).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(nil, clients.ErrNotFound).
+				Once()
+
+			clMock.On("Create", mock.Anything, client).
+				Return(client, nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections: collsMock,
+			Clients:     clMock,
+			Roles:       rMock,
+		}, t)
+
+		err := setup.Install(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to install collection")
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't set schema, storage returns error", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(nil, collections.ErrNotFound).
+				Once()
+
+			collsMock.On("Create", mock.Anything, collection).
+				Return(collection, nil).
+				Once()
+
+			collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema).
+				Return(errors.New("can't set schema")).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(nil, roles.ErrNotFound).
+				Once()
+
+			rMock.On("Create", mock.Anything, mock.Anything).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(nil, clients.ErrNotFound).
+				Once()
+
+			clMock.On("Create", mock.Anything, client).
+				Return(client, nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections: collsMock,
+			Clients:     clMock,
+			Roles:       rMock,
+		}, t)
+
+		err := setup.Install(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to install collection")
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't migrate, storage returns error", func(t *testing.T) {
+		envMocks := &environmentMock.Environments{}
+		envMocks.On("Migrate", mock.Anything, spaceID, envID, &environments.MigrateOptions{Wait: true}).
+			Return(errors.New("can't migrate")).
+			Once()
+
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(nil, collections.ErrNotFound).
+				Once()
+
+			collsMock.On("Create", mock.Anything, collection).
+				Return(collection, nil).
+				Once()
+
+			collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema).
+				Return(nil).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(nil, roles.ErrNotFound).
+				Once()
+
+			rMock.On("Create", mock.Anything, mock.Anything).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(nil, clients.ErrNotFound).
+				Once()
+
+			clMock.On("Create", mock.Anything, client).
+				Return(client, nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections:  collsMock,
+			Clients:      clMock,
+			Roles:        rMock,
+			Environments: envMocks,
+		}, t)
+
+		err := setup.Install(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to install collection")
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't find action, storage returns error", func(t *testing.T) {
+		envMocks := &environmentMock.Environments{}
+		envMocks.On("Migrate", mock.Anything, spaceID, envID, &environments.MigrateOptions{Wait: true}).
+			Return(nil).
+			Once()
+
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(nil, collections.ErrNotFound).
+				Once()
+
+			collsMock.On("Create", mock.Anything, collection).
+				Return(collection, nil).
+				Once()
+
+			collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema).
+				Return(nil).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).Return(role, nil).Once()
+			rMock.On("Update", mock.Anything, &roles.Role{ID: "role", SpaceID: "sp", Environments: []string{"env"}}).Return(nil).Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once()
+
+			clMock.On("Create", mock.Anything, client).
+				Return(client, nil).
+				Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		itmMock.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+			Return(nil, 0, nil).
+			Once()
+
+		itmMock.On("Create", mock.Anything, mock.Anything).
+			Return(nil, errors.New("can't create item")).
+			Once()
+
+		setup := newSetup(&content.Content{
+			Collections:  collsMock,
+			Clients:      clMock,
+			Roles:        rMock,
+			Items:        itmMock,
+			Environments: envMocks,
+		}, t)
+		setup = setup.WithForce(true)
+
+		err := setup.Install(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to install item")
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+		itmMock.AssertExpectations(t)
+		envMocks.AssertExpectations(t)
+	})
+
+	//t.Run("Can't find task configs, storage returns error", func(t *testing.T) {
+	//	envMocks := &environmentMock.Environments{}
+	//	envMocks.On("Migrate", mock.Anything, spaceID, envID).
+	//		Return(nil).
+	//		Once()
+	//
+	//	collsMock := &collectionMock.Collections{}
+	//	for _, collection := range getCollections() {
+	//		collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+	//			Return(nil, collections.ErrNotFound).
+	//			Once()
+	//
+	//		collsMock.On("Create", mock.Anything, collection).
+	//			Return(collection, nil).
+	//			Once()
+	//
+	//		collsMock.On("SetSchema", mock.Anything, spaceID, envID, collection.ID, collection.Schema).
+	//			Return(nil).
+	//			Once()
+	//	}
+	//
+	//	rMock := &rolesMock.Roles{}
+	//	for _, role := range getRoles() {
+	//		rMock.On("Get", mock.Anything, spaceID, role.ID).
+	//			Return(nil, roles.ErrNotFound).
+	//			Once()
+	//
+	//		rMock.On("Create", mock.Anything, mock.Anything).
+	//			Return(role, nil).
+	//			Once()
+	//	}
+	//
+	//	clMock := &clientsMock.Clients{}
+	//	for _, client := range getClients() {
+	//		clMock.On("Get", mock.Anything, spaceID, client.ID).
+	//			Return(nil, clients.ErrNotFound).
+	//			Once()
+	//
+	//		clMock.On("Create", mock.Anything, client).
+	//			Return(client, nil).
+	//			Once()
+	//	}
+	//
+	//	itmMock := &itemsMock.Items{}
+	//	for _, act := range getActions() {
+	//		itmMock.On("Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID).
+	//			Return(nil, items.ErrNotFound).
+	//			Once()
+	//
+	//		itmMock.On("Create", mock.Anything, act).
+	//			Return(act, nil).
+	//			Once()
+	//		itmMock.On("Publish", mock.Anything, act).
+	//			Return(nil).
+	//			Once()
+	//	}
+	//
+	//	setup := newSetup(&content.Content{
+	//		Collections:  collsMock,
+	//		Clients:      clMock,
+	//		Roles:        rMock,
+	//		Items:        itmMock,
+	//		Environments: envMocks,
+	//	}, t)
+	//
+	//	err := setup.Install(context.Background())
+	//
+	//	require.Error(t, err)
+	//	assert.ErrorContains(t, err, "failed to install task configs")
+	//
+	//	rMock.AssertExpectations(t)
+	//	collsMock.AssertExpectations(t)
+	//	clMock.AssertExpectations(t)
+	//	itmMock.AssertExpectations(t)
+	//	envMocks.AssertExpectations(t)
+	//})
+}
+
+func TestSetupUninstall(t *testing.T) {
+	t.Run("Success, nothing to uninstall", func(t *testing.T) {
+		logger := zaptest.NewLogger(t, zaptest.WrapOptions())
+
+		setup := NewSetup(nil, spaceID, envID, logger)
+
+		err := setup.Uninstall(context.Background())
+
+		require.NoError(t, err)
+	})
+
+	t.Run("Remove", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Delete", mock.Anything, spaceID, envID, collection.ID).
+				Return(nil).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Delete", mock.Anything, spaceID, role.ID).
+				Return(nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		for _, act := range getActions() {
+			itmMock.On("Delete", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				del := args[1].(*items.Item)
+				require.Equal(t, spaceID, del.SpaceID)
+				require.Equal(t, envID, del.EnvID)
+				require.Equal(t, extension.ActionsCollectionID, del.CollectionID)
+				require.Equal(t, act.ID, del.ID)
+			}).
+				Return(nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections: collsMock,
+			Clients:     clMock,
+			Roles:       rMock,
+			Items:       itmMock,
+		}, t)
+
+		setup = setup.WithRemove(true)
+		err := setup.Uninstall(context.Background())
+
+		require.NoError(t, err)
+		require.False(t, setup.HasErrors())
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+		itmMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't uninstall clients, storage returns error", func(t *testing.T) {
+		rMock := &rolesMock.Roles{}
+
+		clMock := &clientsMock.Clients{}
+		clMock.On("Delete", mock.Anything, spaceID, mock.Anything).
+			Return(errors.New("can't delete client")).
+			Once()
+
+		itmMock := &itemsMock.Items{}
+		for _, act := range getActions() {
+			itmMock.On("Delete", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID).
+				Return(nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Clients: clMock,
+			Roles:   rMock,
+			Items:   itmMock,
+		}, t)
+
+		setup = setup.WithRemove(true)
+		err := setup.Uninstall(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to uninstall client")
+
+		rMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't uninstall role, storage returns error", func(t *testing.T) {
+		rMock := &rolesMock.Roles{}
+		rMock.On("Delete", mock.Anything, spaceID, mock.Anything).
+			Return(errors.New("can't delete role")).
+			Once()
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		for _, act := range getActions() {
+			itmMock.On("Delete", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID).
+				Return(nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Roles:   rMock,
+			Clients: clMock,
+			Items:   itmMock,
+		}, t)
+
+		setup = setup.WithRemove(true)
+		err := setup.Uninstall(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to uninstall role")
+
+		rMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't uninstall collections, storage returns error", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		collsMock.On("Delete", mock.Anything, spaceID, envID, mock.Anything).
+			Return(errors.New("can't delete collection")).
+			Once()
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Delete", mock.Anything, spaceID, role.ID).
+				Return(nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		for _, act := range getActions() {
+			itmMock.On("Delete", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID).
+				Return(nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections: collsMock,
+			Clients:     clMock,
+			Roles:       rMock,
+			Items:       itmMock,
+		}, t)
+
+		setup = setup.WithRemove(true)
+		err := setup.Uninstall(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to uninstall collection")
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't uninstall actions, storage returns error", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		collsMock.On("Delete", mock.Anything, spaceID, envID, mock.Anything).
+			Return(nil).
+			Once()
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Delete", mock.Anything, spaceID, role.ID).
+				Return(nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Delete", mock.Anything, spaceID, client.ID).Return(nil).Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		for _, act := range getActions() {
+			itmMock.On("Delete", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				del := args[1].(*items.Item)
+				require.Equal(t, spaceID, del.SpaceID)
+				require.Equal(t, envID, del.EnvID)
+				require.Equal(t, extension.ActionsCollectionID, del.CollectionID)
+				require.Equal(t, act.ID, del.ID)
+			}).
+				Return(errors.New("can't delete item")).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Collections: collsMock,
+			Clients:     clMock,
+			Roles:       rMock,
+			Items:       itmMock,
+		}, t)
+
+		setup = setup.WithRemove(true)
+		err := setup.Uninstall(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "failed to uninstall item")
+
+		itmMock.AssertExpectations(t)
+	})
+}
+
+func TestSetupCheck(t *testing.T) {
+	t.Run("Success, nothing to check", func(t *testing.T) {
+		logger := zaptest.NewLogger(t, zaptest.WrapOptions())
+
+		setup := NewSetup(nil, spaceID, envID, logger)
+
+		err := setup.Check(context.Background())
+
+		require.NoError(t, err)
+	})
+
+	t.Run("Success", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(collection, nil).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(client, nil).
+				Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		itmMock.On(
+			"Find",
+			mock.Anything,
+			spaceID,
+			envID,
+			extension.ActionsCollectionID,
+			mock.MatchedBy(func(filter *items.Filter) bool { return data.Contains("act", filter.ID) }),
+			mock.MatchedBy(func(opt *items.FindOptions) bool { return opt.Regular && opt.Hidden && opt.Templates }),
+		).Return(getActions(), 0, nil).Once()
+
+		setup := newSetup(&content.Content{
+			Collections: collsMock,
+			Clients:     clMock,
+			Roles:       rMock,
+			Items:       itmMock,
+		}, t)
+
+		err := setup.Check(context.Background())
+
+		require.NoError(t, err)
+		require.False(t, setup.HasErrors())
+
+		rMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+		itmMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't get role, storage returns error", func(t *testing.T) {
+		rMock := &rolesMock.Roles{}
+		rMock.On("Get", mock.Anything, spaceID, mock.Anything).
+			Return(nil, errors.New("can't get role")).
+			Once()
+
+		setup := newSetup(&content.Content{
+			Roles: rMock,
+		}, t)
+
+		err := setup.Check(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "role check error")
+
+		rMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't get client, storage returns error", func(t *testing.T) {
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		clMock.On("Get", mock.Anything, spaceID, mock.Anything).
+			Return(nil, errors.New("can't get client")).
+			Once()
+
+		setup := newSetup(&content.Content{
+			Roles:   rMock,
+			Clients: clMock,
+		}, t)
+
+		err := setup.Check(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "clients check error")
+
+		rMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't get collection, storage returns error", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		collsMock.On("Get", mock.Anything, spaceID, envID, mock.Anything).
+			Return(nil, errors.New("can't get collection")).
+			Once()
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(client, nil).
+				Once()
+		}
+
+		setup := newSetup(&content.Content{
+			Roles:       rMock,
+			Clients:     clMock,
+			Collections: collsMock,
+		}, t)
+
+		err := setup.Check(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "collections check error")
+
+		rMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+	})
+
+	t.Run("Can't get action, storage returns error", func(t *testing.T) {
+		collsMock := &collectionMock.Collections{}
+		for _, collection := range getCollections() {
+			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+				Return(collection, nil).
+				Once()
+		}
+
+		rMock := &rolesMock.Roles{}
+		for _, role := range getRoles() {
+			rMock.On("Get", mock.Anything, spaceID, role.ID).
+				Return(role, nil).
+				Once()
+		}
+
+		clMock := &clientsMock.Clients{}
+		for _, client := range getClients() {
+			clMock.On("Get", mock.Anything, spaceID, client.ID).
+				Return(client, nil).
+				Once()
+		}
+
+		itmMock := &itemsMock.Items{}
+		itmMock.On("Find", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+			Return(nil, 0, nil).
+			Once()
+
+		setup := newSetup(&content.Content{
+			Roles:       rMock,
+			Clients:     clMock,
+			Collections: collsMock,
+			Items:       itmMock,
+		}, t)
+
+		err := setup.Check(context.Background())
+
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "items check error")
+
+		rMock.AssertExpectations(t)
+		clMock.AssertExpectations(t)
+		collsMock.AssertExpectations(t)
+	})
+
+	//t.Run("Can't get task config, storage returns error", func(t *testing.T) {
+	//	collsMock := &collectionMock.Collections{}
+	//	for _, collection := range getCollections() {
+	//		collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
+	//			Return(collection, nil).
+	//			Once()
+	//	}
+	//
+	//	rMock := &rolesMock.Roles{}
+	//	for _, role := range getRoles() {
+	//		rMock.On("Get", mock.Anything, spaceID, role.ID).
+	//			Return(role, nil).
+	//			Once()
+	//	}
+	//
+	//	clMock := &clientsMock.Clients{}
+	//	for _, client := range getClients() {
+	//		clMock.On("Get", mock.Anything, spaceID, client.ID).
+	//			Return(client, nil).
+	//			Once()
+	//	}
+	//
+	//	itmMock := &itemsMock.Items{}
+	//	for _, act := range getActions() {
+	//		itmMock.On("Get", mock.Anything, spaceID, envID, extension.ActionsCollectionID, act.ID).
+	//			Return(act, nil).
+	//			Once()
+	//	}
+	//
+	//	itmMock.On(
+	//		"Find", mock.Anything, spaceID, envID, tasks.TaskConfigCollection,
+	//		mock.Anything,
+	//	).
+	//		Return(nil, 0, errors.New("can't get task configs")).
+	//		Once()
+	//
+	//	setup := newSetup(&content.Content{
+	//		Roles:       rMock,
+	//		Clients:     clMock,
+	//		Collections: collsMock,
+	//		Items:       itmMock,
+	//	}, t)
+	//
+	//	err := setup.Check(context.Background())
+	//
+	//	require.Error(t, err)
+	//	assert.ErrorContains(t, err, "task configs check error")
+	//
+	//	rMock.AssertExpectations(t)
+	//	clMock.AssertExpectations(t)
+	//	collsMock.AssertExpectations(t)
+	//})
+}