diff --git a/Makefile b/Makefile
index 9a64367046d3ac93fdf4ec05437cfb15ffc43777..b0b31e1dfe312e8ba8ae22c93fd17e5341d60079 100644
--- a/Makefile
+++ b/Makefile
@@ -13,6 +13,9 @@ PROTOGOGRPCFILES=$(PROTOFILES:.proto=_grpc.pb.go)
 PKGDIR=pkg
 ACCESSLOGGING=$(shell find $(PKGDIR) -name "logging_middleware.go" -type f)
 ERRORLOGGING=$(shell find $(PKGDIR) -name "error_logging_middleware.go" -type f)
+SERVICETELEMETRY=$(shell find $(PKGDIR) -name "telemetry_middleware.go" -type f)
+SERVICEMIDDLEWARE=$(shell find $(PKGDIR) -name "middleware.go" -type f)
+SERVICERECOVERING=$(shell find $(PKGDIR) -name "recovering_middleware.go" -type f)
 
 # Генерация grpc-клиентов для go
 proto: protoc-check protoc-gen-go-check $(PROTOGOFILES)
@@ -54,6 +57,24 @@ logging: $(ERRORLOGGING) $(ACCESSLOGGING)
 	@echo "$@"
 	@go generate "$@"
 
+telemetry: $(SERVICETELEMETRY)
+
+%/middleware/telemetry_middleware.go: % .FORCE
+	@echo "$@"
+	@go generate "$@"
+
+middleware: $(SERVICEMIDDLEWARE)
+
+%/middleware/middleware.go: % .FORCE
+	@echo "$@"
+	@go generate "$@"
+
+recovering: $(SERVICERECOVERING)
+
+%/middleware/recovering_middleware.go: % .FORCE
+	@echo "$@"
+	@go generate "$@"
+
 # Генерация моков для всех интерфейсов, найденных в директории. Выходные файлы с моками сохраняются в `./mocks`
 MOCKSDIRS?=$(shell find . -name "service.go" -exec dirname {} \;)
 MOCKS=$(MOCKSDIRS:=/mocks)
diff --git a/assets/templates/middleware/telemetry b/assets/templates/middleware/telemetry
new file mode 100644
index 0000000000000000000000000000000000000000..3e893c09f161d2e832544f8bca625b6d63b3d990
--- /dev/null
+++ b/assets/templates/middleware/telemetry
@@ -0,0 +1,52 @@
+import (
+    "context"
+
+    "go.opentelemetry.io/otel"
+    "go.opentelemetry.io/otel/attribute"
+    "go.opentelemetry.io/otel/trace"
+)
+
+{{ $decorator := (or .Vars.DecoratorName "telemetryMiddleware") }}
+{{ $funcName := (or .Vars.FuncName ("TelemetryMiddleware")) }}
+
+// {{$decorator}} implements {{.Interface.Type}} interface instrumented with opentracing spans
+type {{$decorator}} struct {
+  {{.Interface.Type}}
+  _instance string
+  _spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// {{$funcName}} returns {{$decorator}}
+func {{$funcName}} (base {{.Interface.Type}}, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) {{$decorator}} {
+  d := {{$decorator}} {
+    {{.Interface.Name}}: base,
+    _instance: instance,
+  }
+
+  if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+    d._spanDecorator = spanDecorator[0]
+  }
+
+  return d
+}
+
+{{range $method := .Interface.Methods}}
+  {{if $method.AcceptsContext}}
+    // {{$method.Name}} implements {{$.Interface.Type}}
+func (_d {{$decorator}}) {{$method.Declaration}} {
+  ctx, _span := otel.Tracer(_d._instance).Start(ctx, "{{$.Interface.Name}}.{{$method.Name}}")
+  defer func() {
+    if _d._spanDecorator != nil {
+      _d._spanDecorator(_span, {{$method.ParamsMap}}, {{$method.ResultsMap}})
+    }{{- if $method.ReturnsError}} else if err != nil {
+      _span.RecordError(err)
+      _span.SetAttributes(attribute.String("event", "error"))
+      _span.SetAttributes(attribute.String("message", err.Error()))
+    }
+    {{end}}
+    _span.End()
+  }()
+  {{$method.Pass (printf "_d.%s." $.Interface.Name) }}
+}
+  {{end}}
+{{end}}
\ No newline at end of file
diff --git a/pkg/clients/middleware/telemetry_middleware.go b/pkg/clients/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..d8536b10fcac0a1f237c688fe9a8bde3db93fdcf
--- /dev/null
+++ b/pkg/clients/middleware/telemetry_middleware.go
@@ -0,0 +1,186 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements clients.Clients interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	clients.Clients
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base clients.Clients, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Clients:   base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Create implements clients.Clients
+func (_d telemetryMiddleware) Create(ctx context.Context, client *clients.Client) (created *clients.Client, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Clients.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"client": client}, map[string]interface{}{
+				"created": created,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Clients.Create(ctx, client)
+}
+
+// Delete implements clients.Clients
+func (_d telemetryMiddleware) Delete(ctx context.Context, spaceId string, id string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Clients.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"id":      id}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Clients.Delete(ctx, spaceId, id)
+}
+
+// Enable implements clients.Clients
+func (_d telemetryMiddleware) Enable(ctx context.Context, spaceId string, id string, enable bool) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Clients.Enable")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"id":      id,
+				"enable":  enable}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Clients.Enable(ctx, spaceId, id, enable)
+}
+
+// Get implements clients.Clients
+func (_d telemetryMiddleware) Get(ctx context.Context, spaceId string, id string) (client *clients.Client, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Clients.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"id":      id}, map[string]interface{}{
+				"client": client,
+				"err":    err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Clients.Get(ctx, spaceId, id)
+}
+
+// GetBy implements clients.Clients
+func (_d telemetryMiddleware) GetBy(ctx context.Context, spaceId string, params *clients.GetByParams) (client *clients.Client, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Clients.GetBy")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"params":  params}, map[string]interface{}{
+				"client": client,
+				"err":    err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Clients.GetBy(ctx, spaceId, params)
+}
+
+// List implements clients.Clients
+func (_d telemetryMiddleware) List(ctx context.Context, spaceId string) (clients []*clients.Client, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Clients.List")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId}, map[string]interface{}{
+				"clients": clients,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Clients.List(ctx, spaceId)
+}
+
+// Update implements clients.Clients
+func (_d telemetryMiddleware) Update(ctx context.Context, client *clients.Client) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Clients.Update")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"client": client}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Clients.Update(ctx, client)
+}
diff --git a/pkg/clients/mocks/Storage.go b/pkg/clients/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..43166e0d3f4a21bb80d5179e23d0ee87596a565e
--- /dev/null
+++ b/pkg/clients/mocks/Storage.go
@@ -0,0 +1,156 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	clients "git.perx.ru/perxis/perxis-go/pkg/clients"
+
+	mock "github.com/stretchr/testify/mock"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, client
+func (_m *Storage) Create(ctx context.Context, client *clients.Client) (*clients.Client, error) {
+	ret := _m.Called(ctx, client)
+
+	var r0 *clients.Client
+	if rf, ok := ret.Get(0).(func(context.Context, *clients.Client) *clients.Client); ok {
+		r0 = rf(ctx, client)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*clients.Client)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *clients.Client) error); ok {
+		r1 = rf(ctx, client)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, spaceId, id
+func (_m *Storage) Delete(ctx context.Context, spaceId string, id string) error {
+	ret := _m.Called(ctx, spaceId, id)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceId, id)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Find provides a mock function with given fields: ctx, spaceId, filter, opts
+func (_m *Storage) Find(ctx context.Context, spaceId string, filter *clients.Filter, opts *options.FindOptions) ([]*clients.Client, int, error) {
+	ret := _m.Called(ctx, spaceId, filter, opts)
+
+	var r0 []*clients.Client
+	if rf, ok := ret.Get(0).(func(context.Context, string, *clients.Filter, *options.FindOptions) []*clients.Client); ok {
+		r0 = rf(ctx, spaceId, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*clients.Client)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, string, *clients.Filter, *options.FindOptions) int); ok {
+		r1 = rf(ctx, spaceId, filter, opts)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, string, *clients.Filter, *options.FindOptions) error); ok {
+		r2 = rf(ctx, spaceId, filter, opts)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// Init provides a mock function with given fields: ctx, spaceID
+func (_m *Storage) Init(ctx context.Context, spaceID string) error {
+	ret := _m.Called(ctx, spaceID)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, spaceID)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Reset provides a mock function with given fields: ctx, spaceId
+func (_m *Storage) Reset(ctx context.Context, spaceId string) error {
+	ret := _m.Called(ctx, spaceId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, spaceId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, upd
+func (_m *Storage) Update(ctx context.Context, upd *clients.Client) (int, int, error) {
+	ret := _m.Called(ctx, upd)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *clients.Client) int); ok {
+		r0 = rf(ctx, upd)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *clients.Client) int); ok {
+		r1 = rf(ctx, upd)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *clients.Client) error); ok {
+		r2 = rf(ctx, upd)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/clients/storage.go b/pkg/clients/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..8b9d536a187145687fcfb3e4fc8b481eed77076f
--- /dev/null
+++ b/pkg/clients/storage.go
@@ -0,0 +1,24 @@
+package clients
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Storage interface {
+	Create(ctx context.Context, client *Client) (*Client, error)
+	Find(ctx context.Context, spaceId string, filter *Filter, opts *options.FindOptions) ([]*Client, int, error)
+	Update(ctx context.Context, upd *Client) (updated, total int, err error)
+	Delete(ctx context.Context, spaceId, id string) error
+
+	Reset(ctx context.Context, spaceId string) error
+	Init(ctx context.Context, spaceID string) error
+}
+
+type Filter struct {
+	ID   string
+	Name string
+
+	GetByParams
+}
diff --git a/pkg/collaborators/middleware/telemetry_middleware.go b/pkg/collaborators/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..6a824f1e0a31c585d07b7ebb1bbf391963b0ca63
--- /dev/null
+++ b/pkg/collaborators/middleware/telemetry_middleware.go
@@ -0,0 +1,144 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collaborators -i Collaborators -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements collaborators.Collaborators interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	collaborators.Collaborators
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base collaborators.Collaborators, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Collaborators: base,
+		_instance:     instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Get implements collaborators.Collaborators
+func (_d telemetryMiddleware) Get(ctx context.Context, spaceId string, subject string) (role string, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collaborators.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"subject": subject}, map[string]interface{}{
+				"role": role,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collaborators.Get(ctx, spaceId, subject)
+}
+
+// ListCollaborators implements collaborators.Collaborators
+func (_d telemetryMiddleware) ListCollaborators(ctx context.Context, spaceId string) (collaborators []*collaborators.Collaborator, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collaborators.ListCollaborators")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId}, map[string]interface{}{
+				"collaborators": collaborators,
+				"err":           err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collaborators.ListCollaborators(ctx, spaceId)
+}
+
+// ListSpaces implements collaborators.Collaborators
+func (_d telemetryMiddleware) ListSpaces(ctx context.Context, subject string) (spaces []*collaborators.Collaborator, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collaborators.ListSpaces")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"subject": subject}, map[string]interface{}{
+				"spaces": spaces,
+				"err":    err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collaborators.ListSpaces(ctx, subject)
+}
+
+// Remove implements collaborators.Collaborators
+func (_d telemetryMiddleware) Remove(ctx context.Context, spaceId string, subject string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collaborators.Remove")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"subject": subject}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collaborators.Remove(ctx, spaceId, subject)
+}
+
+// Set implements collaborators.Collaborators
+func (_d telemetryMiddleware) Set(ctx context.Context, spaceId string, subject string, role string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collaborators.Set")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"subject": subject,
+				"role":    role}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collaborators.Set(ctx, spaceId, subject, role)
+}
diff --git a/pkg/collaborators/mocks/CollaboratorObserver.go b/pkg/collaborators/mocks/CollaboratorObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..c7f5aed4d5525f2d9136ea7645696dba9418e785
--- /dev/null
+++ b/pkg/collaborators/mocks/CollaboratorObserver.go
@@ -0,0 +1,25 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// CollaboratorObserver is an autogenerated mock type for the CollaboratorObserver type
+type CollaboratorObserver struct {
+	mock.Mock
+}
+
+type mockConstructorTestingTNewCollaboratorObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewCollaboratorObserver creates a new instance of CollaboratorObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewCollaboratorObserver(t mockConstructorTestingTNewCollaboratorObserver) *CollaboratorObserver {
+	mock := &CollaboratorObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/collaborators/mocks/CollaboratorRemoveObserver.go b/pkg/collaborators/mocks/CollaboratorRemoveObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..8733c7430a5b0a7b4c8b5d34a2a09bf90373a7c9
--- /dev/null
+++ b/pkg/collaborators/mocks/CollaboratorRemoveObserver.go
@@ -0,0 +1,52 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	collaborators "git.perx.ru/perxis/perxis-go/pkg/collaborators"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// CollaboratorRemoveObserver is an autogenerated mock type for the CollaboratorRemoveObserver type
+type CollaboratorRemoveObserver struct {
+	mock.Mock
+}
+
+// OnCollaboratorRemove provides a mock function with given fields: ctx, collaborator
+func (_m *CollaboratorRemoveObserver) OnCollaboratorRemove(ctx context.Context, collaborator *collaborators.Collaborator) (string, error) {
+	ret := _m.Called(ctx, collaborator)
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func(context.Context, *collaborators.Collaborator) string); ok {
+		r0 = rf(ctx, collaborator)
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collaborators.Collaborator) error); ok {
+		r1 = rf(ctx, collaborator)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+type mockConstructorTestingTNewCollaboratorRemoveObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewCollaboratorRemoveObserver creates a new instance of CollaboratorRemoveObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewCollaboratorRemoveObserver(t mockConstructorTestingTNewCollaboratorRemoveObserver) *CollaboratorRemoveObserver {
+	mock := &CollaboratorRemoveObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/collaborators/mocks/CollaboratorSetObserver.go b/pkg/collaborators/mocks/CollaboratorSetObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..296c37f12fd206923b904386a6a15924992fe3f7
--- /dev/null
+++ b/pkg/collaborators/mocks/CollaboratorSetObserver.go
@@ -0,0 +1,52 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	collaborators "git.perx.ru/perxis/perxis-go/pkg/collaborators"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// CollaboratorSetObserver is an autogenerated mock type for the CollaboratorSetObserver type
+type CollaboratorSetObserver struct {
+	mock.Mock
+}
+
+// OnCollaboratorSet provides a mock function with given fields: ctx, collaborator
+func (_m *CollaboratorSetObserver) OnCollaboratorSet(ctx context.Context, collaborator *collaborators.Collaborator) (string, error) {
+	ret := _m.Called(ctx, collaborator)
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func(context.Context, *collaborators.Collaborator) string); ok {
+		r0 = rf(ctx, collaborator)
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collaborators.Collaborator) error); ok {
+		r1 = rf(ctx, collaborator)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+type mockConstructorTestingTNewCollaboratorSetObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewCollaboratorSetObserver creates a new instance of CollaboratorSetObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewCollaboratorSetObserver(t mockConstructorTestingTNewCollaboratorSetObserver) *CollaboratorSetObserver {
+	mock := &CollaboratorSetObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/collaborators/mocks/Storage.go b/pkg/collaborators/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..b2645f347aba5eeb418a5fb4228944bd051e46b0
--- /dev/null
+++ b/pkg/collaborators/mocks/Storage.go
@@ -0,0 +1,119 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	collaborators "git.perx.ru/perxis/perxis-go/pkg/collaborators"
+
+	mock "github.com/stretchr/testify/mock"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Delete provides a mock function with given fields: ctx, filter
+func (_m *Storage) Delete(ctx context.Context, filter *collaborators.Filter) (int, error) {
+	ret := _m.Called(ctx, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *collaborators.Filter) int); ok {
+		r0 = rf(ctx, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collaborators.Filter) error); ok {
+		r1 = rf(ctx, filter)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Find provides a mock function with given fields: ctx, filter, opts
+func (_m *Storage) Find(ctx context.Context, filter *collaborators.Filter, opts *options.FindOptions) ([]*collaborators.Collaborator, error) {
+	ret := _m.Called(ctx, filter, opts)
+
+	var r0 []*collaborators.Collaborator
+	if rf, ok := ret.Get(0).(func(context.Context, *collaborators.Filter, *options.FindOptions) []*collaborators.Collaborator); ok {
+		r0 = rf(ctx, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*collaborators.Collaborator)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collaborators.Filter, *options.FindOptions) error); ok {
+		r1 = rf(ctx, filter, opts)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Init provides a mock function with given fields: ctx
+func (_m *Storage) Init(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Reset provides a mock function with given fields: ctx
+func (_m *Storage) Reset(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Set provides a mock function with given fields: ctx, collaborator
+func (_m *Storage) Set(ctx context.Context, collaborator *collaborators.Collaborator) error {
+	ret := _m.Called(ctx, collaborator)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *collaborators.Collaborator) error); ok {
+		r0 = rf(ctx, collaborator)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/collaborators/storage.go b/pkg/collaborators/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..7eb4bb7d3bf0ed2d3a657d874a2cddc87c4c9bb3
--- /dev/null
+++ b/pkg/collaborators/storage.go
@@ -0,0 +1,22 @@
+package collaborators
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Storage interface {
+	Set(ctx context.Context, collaborator *Collaborator) (err error)
+	Delete(ctx context.Context, filter *Filter) (total int, err error)
+	Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (collaborators []*Collaborator, err error)
+
+	Reset(ctx context.Context) error
+	Init(ctx context.Context) error
+}
+
+type Filter struct {
+	SpaceID string `bson:"spaceId"`
+	Subject string `bson:"userId"`
+	Role    string `bson:"role"`
+}
diff --git a/pkg/collections/middleware/telemetry_middleware.go b/pkg/collections/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..74a0d685c37c6b6d8bdb0b6453f50cfea83e7f07
--- /dev/null
+++ b/pkg/collections/middleware/telemetry_middleware.go
@@ -0,0 +1,194 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements collections.Collections interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	collections.Collections
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base collections.Collections, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Collections: base,
+		_instance:   instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Create implements collections.Collections
+func (_d telemetryMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collections.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":        ctx,
+				"collection": collection}, map[string]interface{}{
+				"created": created,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collections.Create(ctx, collection)
+}
+
+// Delete implements collections.Collections
+func (_d telemetryMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collections.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collections.Delete(ctx, spaceId, envId, collectionId)
+}
+
+// Get implements collections.Collections
+func (_d telemetryMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collections.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"options":      options}, map[string]interface{}{
+				"collection": collection,
+				"err":        err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collections.Get(ctx, spaceId, envId, collectionId, options...)
+}
+
+// List implements collections.Collections
+func (_d telemetryMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collections.List")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"envId":   envId,
+				"filter":  filter}, map[string]interface{}{
+				"collections": collections,
+				"err":         err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collections.List(ctx, spaceId, envId, filter)
+}
+
+// SetSchema implements collections.Collections
+func (_d telemetryMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collections.SetSchema")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"schema":       schema}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collections.SetSchema(ctx, spaceId, envId, collectionId, schema)
+}
+
+// SetState implements collections.Collections
+func (_d telemetryMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collections.SetState")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"state":        state}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collections.SetState(ctx, spaceId, envId, collectionId, state)
+}
+
+// Update implements collections.Collections
+func (_d telemetryMiddleware) Update(ctx context.Context, coll *collections.Collection) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Collections.Update")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":  ctx,
+				"coll": coll}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Collections.Update(ctx, coll)
+}
diff --git a/pkg/collections/mocks/Storage.go b/pkg/collections/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..eb171b5884308f01753827496de9569a575ff818
--- /dev/null
+++ b/pkg/collections/mocks/Storage.go
@@ -0,0 +1,170 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	collections "git.perx.ru/perxis/perxis-go/pkg/collections"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, coll
+func (_m *Storage) Create(ctx context.Context, coll *collections.Collection) (*collections.Collection, error) {
+	ret := _m.Called(ctx, coll)
+
+	var r0 *collections.Collection
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection) *collections.Collection); ok {
+		r0 = rf(ctx, coll)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*collections.Collection)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection) error); ok {
+		r1 = rf(ctx, coll)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, spaceId, envId, collectionId
+func (_m *Storage) Delete(ctx context.Context, spaceId string, envId string, collectionId string) error {
+	ret := _m.Called(ctx, spaceId, envId, collectionId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Get provides a mock function with given fields: ctx, spaceId, envId, collectionId
+func (_m *Storage) Get(ctx context.Context, spaceId string, envId string, collectionId string) (*collections.Collection, error) {
+	ret := _m.Called(ctx, spaceId, envId, collectionId)
+
+	var r0 *collections.Collection
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string) *collections.Collection); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*collections.Collection)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok {
+		r1 = rf(ctx, spaceId, envId, collectionId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Init provides a mock function with given fields: ctx, spaceID, envID
+func (_m *Storage) Init(ctx context.Context, spaceID string, envID string) error {
+	ret := _m.Called(ctx, spaceID, envID)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceID, envID)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// List provides a mock function with given fields: ctx, spaceId, envId, filter
+func (_m *Storage) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) ([]*collections.Collection, error) {
+	ret := _m.Called(ctx, spaceId, envId, filter)
+
+	var r0 []*collections.Collection
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, *collections.Filter) []*collections.Collection); ok {
+		r0 = rf(ctx, spaceId, envId, filter)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*collections.Collection)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, string, *collections.Filter) error); ok {
+		r1 = rf(ctx, spaceId, envId, filter)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Reset provides a mock function with given fields: ctx, spaceId, envId
+func (_m *Storage) Reset(ctx context.Context, spaceId string, envId string) error {
+	ret := _m.Called(ctx, spaceId, envId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, coll
+func (_m *Storage) Update(ctx context.Context, coll *collections.Collection) (int, int, error) {
+	ret := _m.Called(ctx, coll)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection) int); ok {
+		r0 = rf(ctx, coll)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection) int); ok {
+		r1 = rf(ctx, coll)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *collections.Collection) error); ok {
+		r2 = rf(ctx, coll)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/collections/observer.go b/pkg/collections/observer.go
new file mode 100644
index 0000000000000000000000000000000000000000..8d4a6b7d14ec687c97eec6b0406fcc32d515aca2
--- /dev/null
+++ b/pkg/collections/observer.go
@@ -0,0 +1,33 @@
+package collections
+
+import "context"
+
+// CollectionCreatedObserver интерфейс наблюдателя вызываемый при создании коллекции.
+// Инициировать оповещение наблюдателя может вызов метода `Collection.Create`
+type CollectionCreatedObserver interface {
+	OnCollectionCreated(ctx context.Context, coll *Collection) (delayedTaskID string, err error)
+}
+
+// CollectionUpdatedObserver интерфейс наблюдателя вызываемый при изменении коллекции.
+// Инициировать оповещение наблюдателя может вызов методов `Collection.Update`
+type CollectionUpdatedObserver interface {
+	OnCollectionUpdated(ctx context.Context, before, after *Collection) (delayedTaskID string, err error)
+}
+
+// CollectionSetSchemaObserver интерфейс наблюдателя вызываемый при изменении схемы коллекции.
+// Инициировать оповещение наблюдателя может вызов методов `Collection.Schema`
+type CollectionSetSchemaObserver interface {
+	OnCollectionSetSchema(ctx context.Context, before, coll *Collection) (delayedTaskID string, err error)
+}
+
+// CollectionDeletedObserver интерфейс наблюдателя вызываемый при удалении коллекции.
+// Инициировать оповещение наблюдателя может вызов метода `Collection.Delete`
+type CollectionDeletedObserver interface {
+	OnCollectionDeleted(ctx context.Context, coll *Collection) (delayedTaskID string, err error)
+}
+
+// CollectionPreUpdateObserver интерфейс наблюдателя вызываемый при перед обновлением коллекции.
+// Инициировать оповещение наблюдателя может вызов методов `Collection.Update`
+type CollectionPreUpdateObserver interface {
+	OnCollectionPreUpdate(ctx context.Context, before, coll *Collection) (delayedTaskID string, err error)
+}
diff --git a/pkg/collections/storage.go b/pkg/collections/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..37d35b38567e28057099e4edc13c2a1fd74a7817
--- /dev/null
+++ b/pkg/collections/storage.go
@@ -0,0 +1,16 @@
+package collections
+
+import (
+	"context"
+)
+
+type Storage interface {
+	Reset(ctx context.Context, spaceId, envId string) error
+	Init(ctx context.Context, spaceID, envID string) error
+
+	Create(ctx context.Context, coll *Collection) (created *Collection, err error)
+	Get(ctx context.Context, spaceId, envId, collectionId string) (collection *Collection, err error)
+	List(ctx context.Context, spaceId, envId string, filter *Filter) (collections []*Collection, err error)
+	Update(ctx context.Context, coll *Collection) (updated, total int, err error)
+	Delete(ctx context.Context, spaceId, envId, collectionId string) (err error)
+}
diff --git a/pkg/delivery/middleware/telemetry_middleware.go b/pkg/delivery/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..9468782004efd635902eea18b2a174e411a49be4
--- /dev/null
+++ b/pkg/delivery/middleware/telemetry_middleware.go
@@ -0,0 +1,226 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/delivery -i Delivery -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/delivery"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/locales"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements delivery.Delivery interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	delivery.Delivery
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base delivery.Delivery, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Delivery:  base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Aggregate implements delivery.Delivery
+func (_d telemetryMiddleware) Aggregate(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Delivery.Aggregate")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"filter":       filter,
+				"options":      options}, map[string]interface{}{
+				"result": result,
+				"err":    err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Delivery.Aggregate(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+// FindItems implements delivery.Delivery
+func (_d telemetryMiddleware) FindItems(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, total int, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Delivery.FindItems")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"filter":       filter,
+				"options":      options}, map[string]interface{}{
+				"items": items,
+				"total": total,
+				"err":   err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Delivery.FindItems(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+// GetCollection implements delivery.Delivery
+func (_d telemetryMiddleware) GetCollection(ctx context.Context, spaceId string, envId string, collectionId string) (collection *collections.Collection, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Delivery.GetCollection")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId}, map[string]interface{}{
+				"collection": collection,
+				"err":        err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Delivery.GetCollection(ctx, spaceId, envId, collectionId)
+}
+
+// GetEnvironment implements delivery.Delivery
+func (_d telemetryMiddleware) GetEnvironment(ctx context.Context, spaceId string, envId string) (env *environments.Environment, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Delivery.GetEnvironment")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"envId":   envId}, map[string]interface{}{
+				"env": env,
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Delivery.GetEnvironment(ctx, spaceId, envId)
+}
+
+// GetItem implements delivery.Delivery
+func (_d telemetryMiddleware) GetItem(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Delivery.GetItem")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"itemId":       itemId,
+				"options":      options}, map[string]interface{}{
+				"item": item,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Delivery.GetItem(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+// ListCollections implements delivery.Delivery
+func (_d telemetryMiddleware) ListCollections(ctx context.Context, spaceId string, envId string) (collections []*collections.Collection, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Delivery.ListCollections")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"envId":   envId}, map[string]interface{}{
+				"collections": collections,
+				"err":         err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Delivery.ListCollections(ctx, spaceId, envId)
+}
+
+// ListEnvironments implements delivery.Delivery
+func (_d telemetryMiddleware) ListEnvironments(ctx context.Context, spaceId string) (envs []*environments.Environment, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Delivery.ListEnvironments")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId}, map[string]interface{}{
+				"envs": envs,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Delivery.ListEnvironments(ctx, spaceId)
+}
+
+// ListLocales implements delivery.Delivery
+func (_d telemetryMiddleware) ListLocales(ctx context.Context, spaceId string) (locales []*locales.Locale, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Delivery.ListLocales")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId}, map[string]interface{}{
+				"locales": locales,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Delivery.ListLocales(ctx, spaceId)
+}
diff --git a/pkg/environments/middleware/telemetry_middleware.go b/pkg/environments/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..3e27536fb5458dc05dd2f81bbd3ff85830f09c03
--- /dev/null
+++ b/pkg/environments/middleware/telemetry_middleware.go
@@ -0,0 +1,208 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/environments -i Environments -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements environments.Environments interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	environments.Environments
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base environments.Environments, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Environments: base,
+		_instance:    instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Create implements environments.Environments
+func (_d telemetryMiddleware) Create(ctx context.Context, env *environments.Environment) (created *environments.Environment, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Environments.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx": ctx,
+				"env": env}, map[string]interface{}{
+				"created": created,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Environments.Create(ctx, env)
+}
+
+// Delete implements environments.Environments
+func (_d telemetryMiddleware) Delete(ctx context.Context, spaceId string, envId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Environments.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"envId":   envId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Environments.Delete(ctx, spaceId, envId)
+}
+
+// Get implements environments.Environments
+func (_d telemetryMiddleware) Get(ctx context.Context, spaceId string, envId string) (env *environments.Environment, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Environments.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"envId":   envId}, map[string]interface{}{
+				"env": env,
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Environments.Get(ctx, spaceId, envId)
+}
+
+// List implements environments.Environments
+func (_d telemetryMiddleware) List(ctx context.Context, spaceId string) (envs []*environments.Environment, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Environments.List")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId}, map[string]interface{}{
+				"envs": envs,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Environments.List(ctx, spaceId)
+}
+
+// Migrate implements environments.Environments
+func (_d telemetryMiddleware) Migrate(ctx context.Context, spaceId string, envId string, options ...*environments.MigrateOptions) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Environments.Migrate")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"envId":   envId,
+				"options": options}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Environments.Migrate(ctx, spaceId, envId, options...)
+}
+
+// RemoveAlias implements environments.Environments
+func (_d telemetryMiddleware) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Environments.RemoveAlias")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"envId":   envId,
+				"alias":   alias}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Environments.RemoveAlias(ctx, spaceId, envId, alias)
+}
+
+// SetAlias implements environments.Environments
+func (_d telemetryMiddleware) SetAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Environments.SetAlias")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"envId":   envId,
+				"alias":   alias}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Environments.SetAlias(ctx, spaceId, envId, alias)
+}
+
+// Update implements environments.Environments
+func (_d telemetryMiddleware) Update(ctx context.Context, env *environments.Environment) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Environments.Update")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx": ctx,
+				"env": env}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Environments.Update(ctx, env)
+}
diff --git a/pkg/environments/mocks/EnvironmentConfigurationObserver.go b/pkg/environments/mocks/EnvironmentConfigurationObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..e5920c9de716195cbcc1efcd71fbcb75b91d5c40
--- /dev/null
+++ b/pkg/environments/mocks/EnvironmentConfigurationObserver.go
@@ -0,0 +1,44 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	environments "git.perx.ru/perxis/perxis-go/pkg/environments"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// EnvironmentConfigurationObserver is an autogenerated mock type for the EnvironmentConfigurationObserver type
+type EnvironmentConfigurationObserver struct {
+	mock.Mock
+}
+
+// OnEnvironmentConfiguration provides a mock function with given fields: ctx, before, after
+func (_m *EnvironmentConfigurationObserver) OnEnvironmentConfiguration(ctx context.Context, before *environments.Environment, after *environments.Environment) error {
+	ret := _m.Called(ctx, before, after)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment, *environments.Environment) error); ok {
+		r0 = rf(ctx, before, after)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewEnvironmentConfigurationObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewEnvironmentConfigurationObserver creates a new instance of EnvironmentConfigurationObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewEnvironmentConfigurationObserver(t mockConstructorTestingTNewEnvironmentConfigurationObserver) *EnvironmentConfigurationObserver {
+	mock := &EnvironmentConfigurationObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/environments/mocks/EnvironmentCreatedObserver.go b/pkg/environments/mocks/EnvironmentCreatedObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..a2a0eb73e40ef01d0e3732a6011facd3cd4b7abe
--- /dev/null
+++ b/pkg/environments/mocks/EnvironmentCreatedObserver.go
@@ -0,0 +1,51 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	environments "git.perx.ru/perxis/perxis-go/pkg/environments"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// EnvironmentCreatedObserver is an autogenerated mock type for the EnvironmentCreatedObserver type
+type EnvironmentCreatedObserver struct {
+	mock.Mock
+}
+
+// OnEnvironmentCreated provides a mock function with given fields: ctx, env
+func (_m *EnvironmentCreatedObserver) OnEnvironmentCreated(ctx context.Context, env *environments.Environment) (string, error) {
+	ret := _m.Called(ctx, env)
+
+	var r0 string
+	if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment) string); ok {
+		r0 = rf(ctx, env)
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *environments.Environment) error); ok {
+		r1 = rf(ctx, env)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+type mockConstructorTestingTNewEnvironmentCreatedObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewEnvironmentCreatedObserver creates a new instance of EnvironmentCreatedObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewEnvironmentCreatedObserver(t mockConstructorTestingTNewEnvironmentCreatedObserver) *EnvironmentCreatedObserver {
+	mock := &EnvironmentCreatedObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/environments/mocks/EnvironmentDeletedObserver.go b/pkg/environments/mocks/EnvironmentDeletedObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..852cec94086338d53518afff08d18764b88e5170
--- /dev/null
+++ b/pkg/environments/mocks/EnvironmentDeletedObserver.go
@@ -0,0 +1,44 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	environments "git.perx.ru/perxis/perxis-go/pkg/environments"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// EnvironmentDeletedObserver is an autogenerated mock type for the EnvironmentDeletedObserver type
+type EnvironmentDeletedObserver struct {
+	mock.Mock
+}
+
+// OnEnvironmentDeleted provides a mock function with given fields: ctx, env
+func (_m *EnvironmentDeletedObserver) OnEnvironmentDeleted(ctx context.Context, env *environments.Environment) error {
+	ret := _m.Called(ctx, env)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment) error); ok {
+		r0 = rf(ctx, env)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewEnvironmentDeletedObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewEnvironmentDeletedObserver creates a new instance of EnvironmentDeletedObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewEnvironmentDeletedObserver(t mockConstructorTestingTNewEnvironmentDeletedObserver) *EnvironmentDeletedObserver {
+	mock := &EnvironmentDeletedObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/environments/mocks/EnvironmentMigratedObserver.go b/pkg/environments/mocks/EnvironmentMigratedObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..a2f042880ffe88d86187263d1688596d9a878507
--- /dev/null
+++ b/pkg/environments/mocks/EnvironmentMigratedObserver.go
@@ -0,0 +1,44 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	environments "git.perx.ru/perxis/perxis-go/pkg/environments"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// EnvironmentMigratedObserver is an autogenerated mock type for the EnvironmentMigratedObserver type
+type EnvironmentMigratedObserver struct {
+	mock.Mock
+}
+
+// OnEnvironmentMigrated provides a mock function with given fields: ctx, env
+func (_m *EnvironmentMigratedObserver) OnEnvironmentMigrated(ctx context.Context, env *environments.Environment) error {
+	ret := _m.Called(ctx, env)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment) error); ok {
+		r0 = rf(ctx, env)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewEnvironmentMigratedObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewEnvironmentMigratedObserver creates a new instance of EnvironmentMigratedObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewEnvironmentMigratedObserver(t mockConstructorTestingTNewEnvironmentMigratedObserver) *EnvironmentMigratedObserver {
+	mock := &EnvironmentMigratedObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/environments/mocks/EnvironmentObserver.go b/pkg/environments/mocks/EnvironmentObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..309d0ed217317c53be90e4f6a43afcddc3587eb4
--- /dev/null
+++ b/pkg/environments/mocks/EnvironmentObserver.go
@@ -0,0 +1,25 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// EnvironmentObserver is an autogenerated mock type for the EnvironmentObserver type
+type EnvironmentObserver struct {
+	mock.Mock
+}
+
+type mockConstructorTestingTNewEnvironmentObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewEnvironmentObserver creates a new instance of EnvironmentObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewEnvironmentObserver(t mockConstructorTestingTNewEnvironmentObserver) *EnvironmentObserver {
+	mock := &EnvironmentObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/environments/mocks/EnvironmentUpdatedObserver.go b/pkg/environments/mocks/EnvironmentUpdatedObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..4a7e76673439dcf09d9b3b52c461482e637960a3
--- /dev/null
+++ b/pkg/environments/mocks/EnvironmentUpdatedObserver.go
@@ -0,0 +1,44 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	environments "git.perx.ru/perxis/perxis-go/pkg/environments"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// EnvironmentUpdatedObserver is an autogenerated mock type for the EnvironmentUpdatedObserver type
+type EnvironmentUpdatedObserver struct {
+	mock.Mock
+}
+
+// OnEnvironmentUpdated provides a mock function with given fields: ctx, before, after
+func (_m *EnvironmentUpdatedObserver) OnEnvironmentUpdated(ctx context.Context, before *environments.Environment, after *environments.Environment) error {
+	ret := _m.Called(ctx, before, after)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment, *environments.Environment) error); ok {
+		r0 = rf(ctx, before, after)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewEnvironmentUpdatedObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewEnvironmentUpdatedObserver creates a new instance of EnvironmentUpdatedObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewEnvironmentUpdatedObserver(t mockConstructorTestingTNewEnvironmentUpdatedObserver) *EnvironmentUpdatedObserver {
+	mock := &EnvironmentUpdatedObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/environments/mocks/Storage.go b/pkg/environments/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..68411694c5fc03e497e8c37e6a1ff0a1f1a68721
--- /dev/null
+++ b/pkg/environments/mocks/Storage.go
@@ -0,0 +1,195 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	environments "git.perx.ru/perxis/perxis-go/pkg/environments"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, create
+func (_m *Storage) Create(ctx context.Context, create *environments.Environment) (*environments.Environment, error) {
+	ret := _m.Called(ctx, create)
+
+	var r0 *environments.Environment
+	if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment) *environments.Environment); ok {
+		r0 = rf(ctx, create)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*environments.Environment)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *environments.Environment) error); ok {
+		r1 = rf(ctx, create)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, spaceId, envId
+func (_m *Storage) Delete(ctx context.Context, spaceId string, envId string) error {
+	ret := _m.Called(ctx, spaceId, envId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Get provides a mock function with given fields: ctx, spaceId, envId
+func (_m *Storage) Get(ctx context.Context, spaceId string, envId string) (*environments.Environment, error) {
+	ret := _m.Called(ctx, spaceId, envId)
+
+	var r0 *environments.Environment
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) *environments.Environment); ok {
+		r0 = rf(ctx, spaceId, envId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*environments.Environment)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
+		r1 = rf(ctx, spaceId, envId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Init provides a mock function with given fields: ctx, spaceID
+func (_m *Storage) Init(ctx context.Context, spaceID string) error {
+	ret := _m.Called(ctx, spaceID)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, spaceID)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// List provides a mock function with given fields: ctx, spaceId
+func (_m *Storage) List(ctx context.Context, spaceId string) ([]*environments.Environment, error) {
+	ret := _m.Called(ctx, spaceId)
+
+	var r0 []*environments.Environment
+	if rf, ok := ret.Get(0).(func(context.Context, string) []*environments.Environment); ok {
+		r0 = rf(ctx, spaceId)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*environments.Environment)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
+		r1 = rf(ctx, spaceId)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// RemoveAlias provides a mock function with given fields: ctx, spaceId, envId, alias
+func (_m *Storage) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) error {
+	ret := _m.Called(ctx, spaceId, envId, alias)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId, alias)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Reset provides a mock function with given fields: ctx, spaceId
+func (_m *Storage) Reset(ctx context.Context, spaceId string) {
+	_m.Called(ctx, spaceId)
+}
+
+// SetAlias provides a mock function with given fields: ctx, spaceId, envId, alias
+func (_m *Storage) SetAlias(ctx context.Context, spaceId string, envId string, alias string) error {
+	ret := _m.Called(ctx, spaceId, envId, alias)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId, alias)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, up, options
+func (_m *Storage) Update(ctx context.Context, up *environments.Environment, options ...*environments.UpdateOptions) (int, int, error) {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, up)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment, ...*environments.UpdateOptions) int); ok {
+		r0 = rf(ctx, up, options...)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *environments.Environment, ...*environments.UpdateOptions) int); ok {
+		r1 = rf(ctx, up, options...)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *environments.Environment, ...*environments.UpdateOptions) error); ok {
+		r2 = rf(ctx, up, options...)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/environments/observer.go b/pkg/environments/observer.go
new file mode 100644
index 0000000000000000000000000000000000000000..0368d190acec46508eaa356b43da16791e0a780d
--- /dev/null
+++ b/pkg/environments/observer.go
@@ -0,0 +1,41 @@
+package environments
+
+import "context"
+
+type EnvironmentObserver interface{}
+
+// EnvironmentCreatedObserver интерфейс наблюдателя вызываемый при создании окружения.
+// Инициировать оповещение наблюдателя может вызов метода `Environments.Create`
+// Оповещение выполнятся в асинхронном режиме.
+type EnvironmentCreatedObserver interface {
+	OnEnvironmentCreated(ctx context.Context, env *Environment) (delayedTaskID string, err error)
+}
+
+// EnvironmentUpdatedObserver интерфейс наблюдателя вызываемый при изменении окружения.
+// Инициировать оповещение наблюдателя может вызов методов `Environments.Update`,
+// `Environments.SetAlias`, `Environments.RemoveAlias`
+// Оповещение выполнятся в асинхронном режиме.
+type EnvironmentUpdatedObserver interface {
+	OnEnvironmentUpdated(ctx context.Context, before, after *Environment) error
+}
+
+// EnvironmentDeletedObserver интерфейс наблюдателя вызываемый при удалении окружения.
+// Инициировать оповещение наблюдателя может вызов метода `Environments.Delete`.
+// Оповещение выполнятся в асинхронном режиме.
+type EnvironmentDeletedObserver interface {
+	OnEnvironmentDeleted(ctx context.Context, env *Environment) error
+}
+
+// EnvironmentConfigurationObserver интерфейс наблюдателя вызываемый при изменении конфигурации
+// окружения.
+// Инициировать оповещение наблюдателя может вызов методов `Environments.OnSpaceUpdate`,
+// Оповещение выполнятся в асинхронном режиме.
+type EnvironmentConfigurationObserver interface {
+	OnEnvironmentConfiguration(ctx context.Context, before, after *Environment) error
+}
+
+// EnvironmentMigratedObserver интерфейс наблюдателя вызываемый при запуске миграции окружения.
+// Инициировать оповещение наблюдателя может вызов методов `Environments.Migrate`,
+type EnvironmentMigratedObserver interface {
+	OnEnvironmentMigrated(ctx context.Context, env *Environment) error
+}
diff --git a/pkg/environments/storage.go b/pkg/environments/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..675861e634b4e48cd6a17c77bf8c834fb7cd6e3a
--- /dev/null
+++ b/pkg/environments/storage.go
@@ -0,0 +1,16 @@
+package environments
+
+import "context"
+
+type Storage interface {
+	Create(ctx context.Context, create *Environment) (environment *Environment, err error)
+	Get(ctx context.Context, spaceId, envId string) (env *Environment, err error)
+	List(ctx context.Context, spaceId string) (envs []*Environment, err error)
+	Update(ctx context.Context, up *Environment, options ...*UpdateOptions) (updated, total int, err error)
+	Delete(ctx context.Context, spaceId, envId string) (err error)
+	SetAlias(ctx context.Context, spaceId, envId, alias string) (err error)
+	RemoveAlias(ctx context.Context, spaceId, envId, alias string) (err error)
+
+	Reset(ctx context.Context, spaceId string)
+	Init(ctx context.Context, spaceID string) error
+}
diff --git a/pkg/files/middleware/error_logging_middleware.go b/pkg/files/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..18e9b65a502b6cacc1913a9c14cfe5ff0aac8a1c
--- /dev/null
+++ b/pkg/files/middleware/error_logging_middleware.go
@@ -0,0 +1,100 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/files -i Files -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/files"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements files.Files that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   files.Files
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the files.Files with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next files.Files) files.Files {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) AbortUpload(ctx context.Context, upload *files.MultipartUpload) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.AbortUpload(ctx, upload)
+}
+
+func (m *errorLoggingMiddleware) CompleteUpload(ctx context.Context, upload *files.MultipartUpload) (u *files.MultipartUpload, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.CompleteUpload(ctx, upload)
+}
+
+func (m *errorLoggingMiddleware) DeleteFile(ctx context.Context, file *files.File) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.DeleteFile(ctx, file)
+}
+
+func (m *errorLoggingMiddleware) GetFile(ctx context.Context, file *files.File) (f *files.File, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.GetFile(ctx, file)
+}
+
+func (m *errorLoggingMiddleware) MoveUpload(ctx context.Context, upload *files.MultipartUpload) (file *files.File, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.MoveUpload(ctx, upload)
+}
+
+func (m *errorLoggingMiddleware) StartUpload(ctx context.Context, upload *files.MultipartUpload) (u *files.MultipartUpload, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.StartUpload(ctx, upload)
+}
+
+func (m *errorLoggingMiddleware) Upload(ctx context.Context, file *files.File) (u *files.Upload, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Upload(ctx, file)
+}
diff --git a/pkg/files/middleware/logging_middleware.go b/pkg/files/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..b295fa68cc8367021d5fa5e00a0c850cc27c7cee
--- /dev/null
+++ b/pkg/files/middleware/logging_middleware.go
@@ -0,0 +1,291 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/files -i Files -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/files"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements files.Files that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   files.Files
+}
+
+// LoggingMiddleware instruments an implementation of the files.Files with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next files.Files) files.Files {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) AbortUpload(ctx context.Context, upload *files.MultipartUpload) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"upload": upload} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("AbortUpload.Request", fields...)
+
+	err = m.next.AbortUpload(ctx, upload)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			err, _ := v.(error)
+			fields = append(fields, zap.Error(err))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("AbortUpload.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) CompleteUpload(ctx context.Context, upload *files.MultipartUpload) (u *files.MultipartUpload, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"upload": upload} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("CompleteUpload.Request", fields...)
+
+	u, err = m.next.CompleteUpload(ctx, upload)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+	}
+
+	for k, v := range map[string]interface{}{
+		"u":   u,
+		"err": err} {
+		if k == "err" {
+			err, _ := v.(error)
+			fields = append(fields, zap.Error(err))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("CompleteUpload.Response", fields...)
+
+	return u, err
+}
+
+func (m *loggingMiddleware) DeleteFile(ctx context.Context, file *files.File) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":  ctx,
+		"file": file} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("DeleteFile.Request", fields...)
+
+	err = m.next.DeleteFile(ctx, file)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			err, _ := v.(error)
+			fields = append(fields, zap.Error(err))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("DeleteFile.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) GetFile(ctx context.Context, file *files.File) (f *files.File, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":  ctx,
+		"file": file} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetFile.Request", fields...)
+
+	f, err = m.next.GetFile(ctx, file)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+	}
+
+	for k, v := range map[string]interface{}{
+		"f":   f,
+		"err": err} {
+		if k == "err" {
+			err, _ := v.(error)
+			fields = append(fields, zap.Error(err))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetFile.Response", fields...)
+
+	return f, err
+}
+
+func (m *loggingMiddleware) MoveUpload(ctx context.Context, upload *files.MultipartUpload) (file *files.File, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"upload": upload} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("MoveUpload.Request", fields...)
+
+	file, err = m.next.MoveUpload(ctx, upload)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+	}
+
+	for k, v := range map[string]interface{}{
+		"file": file,
+		"err":  err} {
+		if k == "err" {
+			err, _ := v.(error)
+			fields = append(fields, zap.Error(err))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("MoveUpload.Response", fields...)
+
+	return file, err
+}
+
+func (m *loggingMiddleware) StartUpload(ctx context.Context, upload *files.MultipartUpload) (u *files.MultipartUpload, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"upload": upload} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("StartUpload.Request", fields...)
+
+	u, err = m.next.StartUpload(ctx, upload)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+	}
+
+	for k, v := range map[string]interface{}{
+		"u":   u,
+		"err": err} {
+		if k == "err" {
+			err, _ := v.(error)
+			fields = append(fields, zap.Error(err))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("StartUpload.Response", fields...)
+
+	return u, err
+}
+
+func (m *loggingMiddleware) Upload(ctx context.Context, file *files.File) (u *files.Upload, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":  ctx,
+		"file": file} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Upload.Request", fields...)
+
+	u, err = m.next.Upload(ctx, file)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+	}
+
+	for k, v := range map[string]interface{}{
+		"u":   u,
+		"err": err} {
+		if k == "err" {
+			err, _ := v.(error)
+			fields = append(fields, zap.Error(err))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Upload.Response", fields...)
+
+	return u, err
+}
diff --git a/pkg/files/middleware/middleware.go b/pkg/files/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..39c64f8764e904b53fcce18bf8b990424ad03149
--- /dev/null
+++ b/pkg/files/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/files -i Files -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/files"
+	"go.uber.org/zap"
+)
+
+type Middleware func(files.Files) files.Files
+
+func WithLog(s files.Files, logger *zap.Logger, log_access bool) files.Files {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Files")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/files/middleware/recovering_middleware.go b/pkg/files/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..3143498e490013dd747185a339c4111df107dd90
--- /dev/null
+++ b/pkg/files/middleware/recovering_middleware.go
@@ -0,0 +1,115 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/files -i Files -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/files"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements files.Files that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   files.Files
+}
+
+// RecoveringMiddleware instruments an implementation of the files.Files with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next files.Files) files.Files {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) AbortUpload(ctx context.Context, upload *files.MultipartUpload) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.AbortUpload(ctx, upload)
+}
+
+func (m *recoveringMiddleware) CompleteUpload(ctx context.Context, upload *files.MultipartUpload) (u *files.MultipartUpload, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.CompleteUpload(ctx, upload)
+}
+
+func (m *recoveringMiddleware) DeleteFile(ctx context.Context, file *files.File) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.DeleteFile(ctx, file)
+}
+
+func (m *recoveringMiddleware) GetFile(ctx context.Context, file *files.File) (f *files.File, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.GetFile(ctx, file)
+}
+
+func (m *recoveringMiddleware) MoveUpload(ctx context.Context, upload *files.MultipartUpload) (file *files.File, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.MoveUpload(ctx, upload)
+}
+
+func (m *recoveringMiddleware) StartUpload(ctx context.Context, upload *files.MultipartUpload) (u *files.MultipartUpload, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.StartUpload(ctx, upload)
+}
+
+func (m *recoveringMiddleware) Upload(ctx context.Context, file *files.File) (u *files.Upload, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Upload(ctx, file)
+}
diff --git a/pkg/files/middleware/telemetry_middleware.go b/pkg/files/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..21e10e8df709561cc9234df1a2621754c29ebe49
--- /dev/null
+++ b/pkg/files/middleware/telemetry_middleware.go
@@ -0,0 +1,182 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/files -i Files -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/files"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements files.Files interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	files.Files
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base files.Files, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Files:     base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// AbortUpload implements files.Files
+func (_d telemetryMiddleware) AbortUpload(ctx context.Context, upload *files.MultipartUpload) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Files.AbortUpload")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"upload": upload}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Files.AbortUpload(ctx, upload)
+}
+
+// CompleteUpload implements files.Files
+func (_d telemetryMiddleware) CompleteUpload(ctx context.Context, upload *files.MultipartUpload) (u *files.MultipartUpload, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Files.CompleteUpload")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"upload": upload}, map[string]interface{}{
+				"u":   u,
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Files.CompleteUpload(ctx, upload)
+}
+
+// DeleteFile implements files.Files
+func (_d telemetryMiddleware) DeleteFile(ctx context.Context, file *files.File) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Files.DeleteFile")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":  ctx,
+				"file": file}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Files.DeleteFile(ctx, file)
+}
+
+// GetFile implements files.Files
+func (_d telemetryMiddleware) GetFile(ctx context.Context, file *files.File) (f *files.File, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Files.GetFile")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":  ctx,
+				"file": file}, map[string]interface{}{
+				"f":   f,
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Files.GetFile(ctx, file)
+}
+
+// MoveUpload implements files.Files
+func (_d telemetryMiddleware) MoveUpload(ctx context.Context, upload *files.MultipartUpload) (file *files.File, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Files.MoveUpload")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"upload": upload}, map[string]interface{}{
+				"file": file,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Files.MoveUpload(ctx, upload)
+}
+
+// StartUpload implements files.Files
+func (_d telemetryMiddleware) StartUpload(ctx context.Context, upload *files.MultipartUpload) (u *files.MultipartUpload, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Files.StartUpload")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"upload": upload}, map[string]interface{}{
+				"u":   u,
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Files.StartUpload(ctx, upload)
+}
+
+// Upload implements files.Files
+func (_d telemetryMiddleware) Upload(ctx context.Context, file *files.File) (u *files.Upload, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Files.Upload")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":  ctx,
+				"file": file}, map[string]interface{}{
+				"u":   u,
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Files.Upload(ctx, file)
+}
diff --git a/pkg/files/mocks/Middleware.go b/pkg/files/mocks/Middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..4a47061df1e1fe7b868cba7da09a5615cf313985
--- /dev/null
+++ b/pkg/files/mocks/Middleware.go
@@ -0,0 +1,45 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	files "git.perx.ru/perxis/perxis-go/pkg/files"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Middleware is an autogenerated mock type for the Middleware type
+type Middleware struct {
+	mock.Mock
+}
+
+// Execute provides a mock function with given fields: _a0
+func (_m *Middleware) Execute(_a0 files.Files) files.Files {
+	ret := _m.Called(_a0)
+
+	var r0 files.Files
+	if rf, ok := ret.Get(0).(func(files.Files) files.Files); ok {
+		r0 = rf(_a0)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(files.Files)
+		}
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewMiddleware interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewMiddleware creates a new instance of Middleware. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewMiddleware(t mockConstructorTestingTNewMiddleware) *Middleware {
+	mock := &Middleware{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/files/mocks/Storage.go b/pkg/files/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..7d4a411253dc517fb1a034f79035cf85756daccd
--- /dev/null
+++ b/pkg/files/mocks/Storage.go
@@ -0,0 +1,173 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	files "git.perx.ru/perxis/perxis-go/pkg/files"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// AbortUpload provides a mock function with given fields: ctx, upload
+func (_m *Storage) AbortUpload(ctx context.Context, upload *files.MultipartUpload) error {
+	ret := _m.Called(ctx, upload)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *files.MultipartUpload) error); ok {
+		r0 = rf(ctx, upload)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// CompleteUpload provides a mock function with given fields: ctx, upload
+func (_m *Storage) CompleteUpload(ctx context.Context, upload *files.MultipartUpload) (*files.MultipartUpload, error) {
+	ret := _m.Called(ctx, upload)
+
+	var r0 *files.MultipartUpload
+	if rf, ok := ret.Get(0).(func(context.Context, *files.MultipartUpload) *files.MultipartUpload); ok {
+		r0 = rf(ctx, upload)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*files.MultipartUpload)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *files.MultipartUpload) error); ok {
+		r1 = rf(ctx, upload)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// DeleteFile provides a mock function with given fields: ctx, file
+func (_m *Storage) DeleteFile(ctx context.Context, file *files.File) error {
+	ret := _m.Called(ctx, file)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *files.File) error); ok {
+		r0 = rf(ctx, file)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// GetFile provides a mock function with given fields: ctx, file
+func (_m *Storage) GetFile(ctx context.Context, file *files.File) (*files.File, error) {
+	ret := _m.Called(ctx, file)
+
+	var r0 *files.File
+	if rf, ok := ret.Get(0).(func(context.Context, *files.File) *files.File); ok {
+		r0 = rf(ctx, file)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*files.File)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *files.File) error); ok {
+		r1 = rf(ctx, file)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Move provides a mock function with given fields: ctx, src, dst
+func (_m *Storage) Move(ctx context.Context, src *files.File, dst *files.File) (*files.File, error) {
+	ret := _m.Called(ctx, src, dst)
+
+	var r0 *files.File
+	if rf, ok := ret.Get(0).(func(context.Context, *files.File, *files.File) *files.File); ok {
+		r0 = rf(ctx, src, dst)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*files.File)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *files.File, *files.File) error); ok {
+		r1 = rf(ctx, src, dst)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// StartUpload provides a mock function with given fields: ctx, upload
+func (_m *Storage) StartUpload(ctx context.Context, upload *files.MultipartUpload) (*files.MultipartUpload, error) {
+	ret := _m.Called(ctx, upload)
+
+	var r0 *files.MultipartUpload
+	if rf, ok := ret.Get(0).(func(context.Context, *files.MultipartUpload) *files.MultipartUpload); ok {
+		r0 = rf(ctx, upload)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*files.MultipartUpload)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *files.MultipartUpload) error); ok {
+		r1 = rf(ctx, upload)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Upload provides a mock function with given fields: ctx, file
+func (_m *Storage) Upload(ctx context.Context, file *files.File) (*files.Upload, error) {
+	ret := _m.Called(ctx, file)
+
+	var r0 *files.Upload
+	if rf, ok := ret.Get(0).(func(context.Context, *files.File) *files.Upload); ok {
+		r0 = rf(ctx, file)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*files.Upload)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *files.File) error); ok {
+		r1 = rf(ctx, file)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/files/storage.go b/pkg/files/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..085d01f8a21ca3bd922f31b85b137d99209815a6
--- /dev/null
+++ b/pkg/files/storage.go
@@ -0,0 +1,29 @@
+package files
+
+import "context"
+
+type Storage interface {
+	// StartUpload - инициирует процедуру загрузки файла в файловое хранилище.
+	// Используется клиентским приложением для начала загрузки файла
+	StartUpload(ctx context.Context, upload *MultipartUpload) (*MultipartUpload, error)
+
+	// CompleteUpload - завершает процедуру загрузки файла
+	CompleteUpload(ctx context.Context, upload *MultipartUpload) (*MultipartUpload, error)
+
+	// AbortUpload - прерывает процедуру загрузки файла, все загруженные части файла удаляются из хранилища
+	AbortUpload(ctx context.Context, upload *MultipartUpload) error
+
+	// Move - перемещает файл с ключом src.Key по новому пути dst.Key
+	Move(ctx context.Context, src, dst *File) (*File, error)
+
+	// Upload - одиночная загрузка файла в хранилище
+	// Максимальный размер загружаемого файла - 5ГБ
+	Upload(ctx context.Context, file *File) (*Upload, error)
+
+	// GetFile - проверяет, существует ли файл в хранилище и
+	// возвращает объект 'File' с заполненным URL
+	GetFile(ctx context.Context, file *File) (f *File, err error)
+
+	// DeleteFile - удаляет файл по ключу
+	DeleteFile(ctx context.Context, file *File) error
+}
diff --git a/pkg/images/middleware/error_logging_middleware.go b/pkg/images/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..749db73b2ed5682c9725c920ae4b850eff5e67e1
--- /dev/null
+++ b/pkg/images/middleware/error_logging_middleware.go
@@ -0,0 +1,41 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/images -i Images -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/files"
+	"git.perx.ru/perxis/perxis-go/pkg/images"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements images.Images that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   images.Images
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the images.Images with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next images.Images) images.Images {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, source *files.File, opts *images.GetOptions) (result *files.File, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, source, opts)
+}
diff --git a/pkg/images/middleware/logging_middleware.go b/pkg/images/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..fb7d0af4faecfde621e295b0da8e6f2e098e56a1
--- /dev/null
+++ b/pkg/images/middleware/logging_middleware.go
@@ -0,0 +1,73 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/images -i Images -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/files"
+	"git.perx.ru/perxis/perxis-go/pkg/images"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements images.Images that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   images.Images
+}
+
+// LoggingMiddleware instruments an implementation of the images.Images with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next images.Images) images.Images {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, source *files.File, opts *images.GetOptions) (result *files.File, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"source": source,
+		"opts":   opts} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	result, err = m.next.Get(ctx, source, opts)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+	}
+
+	for k, v := range map[string]interface{}{
+		"result": result,
+		"err":    err} {
+		if k == "err" {
+			err, _ := v.(error)
+			fields = append(fields, zap.Error(err))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return result, err
+}
diff --git a/pkg/images/middleware/middleware.go b/pkg/images/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..d4717a0db627b3f11abfc569cd1c79e30300b93f
--- /dev/null
+++ b/pkg/images/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/images -i Images -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/images"
+	"go.uber.org/zap"
+)
+
+type Middleware func(images.Images) images.Images
+
+func WithLog(s images.Images, logger *zap.Logger, log_access bool) images.Images {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Images")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/images/middleware/recovering_middleware.go b/pkg/images/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..4fe31525e0b587e94a6cb8259ae551b38fd4f450
--- /dev/null
+++ b/pkg/images/middleware/recovering_middleware.go
@@ -0,0 +1,44 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/images -i Images -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/files"
+	"git.perx.ru/perxis/perxis-go/pkg/images"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements images.Images that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   images.Images
+}
+
+// RecoveringMiddleware instruments an implementation of the images.Images with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next images.Images) images.Images {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, source *files.File, opts *images.GetOptions) (result *files.File, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, source, opts)
+}
diff --git a/pkg/images/middleware/telemetry_middleware.go b/pkg/images/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..b3cb8bef156613220dac2d1ad01a56bc4bba3723
--- /dev/null
+++ b/pkg/images/middleware/telemetry_middleware.go
@@ -0,0 +1,60 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/images -i Images -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/files"
+	"git.perx.ru/perxis/perxis-go/pkg/images"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements images.Images interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	images.Images
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base images.Images, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Images:    base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Get implements images.Images
+func (_d telemetryMiddleware) Get(ctx context.Context, source *files.File, opts *images.GetOptions) (result *files.File, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Images.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"source": source,
+				"opts":   opts}, map[string]interface{}{
+				"result": result,
+				"err":    err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Images.Get(ctx, source, opts)
+}
diff --git a/pkg/images/mocks/Middleware.go b/pkg/images/mocks/Middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..43ba1dd2b86fd43237c6e83c24a13928a8f8c2d3
--- /dev/null
+++ b/pkg/images/mocks/Middleware.go
@@ -0,0 +1,45 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	images "git.perx.ru/perxis/perxis-go/pkg/images"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Middleware is an autogenerated mock type for the Middleware type
+type Middleware struct {
+	mock.Mock
+}
+
+// Execute provides a mock function with given fields: _a0
+func (_m *Middleware) Execute(_a0 images.Images) images.Images {
+	ret := _m.Called(_a0)
+
+	var r0 images.Images
+	if rf, ok := ret.Get(0).(func(images.Images) images.Images); ok {
+		r0 = rf(_a0)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(images.Images)
+		}
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewMiddleware interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewMiddleware creates a new instance of Middleware. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewMiddleware(t mockConstructorTestingTNewMiddleware) *Middleware {
+	mock := &Middleware{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/invitations/middleware/telemetry_middleware.go b/pkg/invitations/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..cf79342a3ee7ef978c18b1d785ec40e4d47523bc
--- /dev/null
+++ b/pkg/invitations/middleware/telemetry_middleware.go
@@ -0,0 +1,144 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/invitations -i Invitations -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/invitations"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements invitations.Invitations interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	invitations.Invitations
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base invitations.Invitations, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Invitations: base,
+		_instance:   instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Accept implements invitations.Invitations
+func (_d telemetryMiddleware) Accept(ctx context.Context, invitationId string, userId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Invitations.Accept")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"invitationId": invitationId,
+				"userId":       userId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Invitations.Accept(ctx, invitationId, userId)
+}
+
+// Create implements invitations.Invitations
+func (_d telemetryMiddleware) Create(ctx context.Context, invitation *invitations.Invitation) (created *invitations.Invitation, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Invitations.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":        ctx,
+				"invitation": invitation}, map[string]interface{}{
+				"created": created,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Invitations.Create(ctx, invitation)
+}
+
+// Delete implements invitations.Invitations
+func (_d telemetryMiddleware) Delete(ctx context.Context, invitationId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Invitations.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"invitationId": invitationId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Invitations.Delete(ctx, invitationId)
+}
+
+// Find implements invitations.Invitations
+func (_d telemetryMiddleware) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) (invitations []*invitations.Invitation, total int, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Invitations.Find")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"filter": filter,
+				"opts":   opts}, map[string]interface{}{
+				"invitations": invitations,
+				"total":       total,
+				"err":         err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Invitations.Find(ctx, filter, opts)
+}
+
+// Get implements invitations.Invitations
+func (_d telemetryMiddleware) Get(ctx context.Context, invitationId string) (invitation *invitations.Invitation, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Invitations.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"invitationId": invitationId}, map[string]interface{}{
+				"invitation": invitation,
+				"err":        err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Invitations.Get(ctx, invitationId)
+}
diff --git a/pkg/invitations/mocks/Storage.go b/pkg/invitations/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..8739d1e1195931da4dfc05f2e62ad58ca7988365
--- /dev/null
+++ b/pkg/invitations/mocks/Storage.go
@@ -0,0 +1,162 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	invitations "git.perx.ru/perxis/perxis-go/pkg/invitations"
+	mock "github.com/stretchr/testify/mock"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, create
+func (_m *Storage) Create(ctx context.Context, create *invitations.Invitation) (*invitations.Invitation, error) {
+	ret := _m.Called(ctx, create)
+
+	var r0 *invitations.Invitation
+	if rf, ok := ret.Get(0).(func(context.Context, *invitations.Invitation) *invitations.Invitation); ok {
+		r0 = rf(ctx, create)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*invitations.Invitation)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *invitations.Invitation) error); ok {
+		r1 = rf(ctx, create)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, filter
+func (_m *Storage) Delete(ctx context.Context, filter *invitations.Filter) (int, error) {
+	ret := _m.Called(ctx, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *invitations.Filter) int); ok {
+		r0 = rf(ctx, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *invitations.Filter) error); ok {
+		r1 = rf(ctx, filter)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Find provides a mock function with given fields: ctx, filter, opts
+func (_m *Storage) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) ([]*invitations.Invitation, int, error) {
+	ret := _m.Called(ctx, filter, opts)
+
+	var r0 []*invitations.Invitation
+	if rf, ok := ret.Get(0).(func(context.Context, *invitations.Filter, *options.FindOptions) []*invitations.Invitation); ok {
+		r0 = rf(ctx, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*invitations.Invitation)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *invitations.Filter, *options.FindOptions) int); ok {
+		r1 = rf(ctx, filter, opts)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *invitations.Filter, *options.FindOptions) error); ok {
+		r2 = rf(ctx, filter, opts)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// Init provides a mock function with given fields: ctx
+func (_m *Storage) Init(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Reset provides a mock function with given fields: ctx
+func (_m *Storage) Reset(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, update, filter
+func (_m *Storage) Update(ctx context.Context, update *invitations.Invitation, filter *invitations.Filter) (int, int, error) {
+	ret := _m.Called(ctx, update, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *invitations.Invitation, *invitations.Filter) int); ok {
+		r0 = rf(ctx, update, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *invitations.Invitation, *invitations.Filter) int); ok {
+		r1 = rf(ctx, update, filter)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *invitations.Invitation, *invitations.Filter) error); ok {
+		r2 = rf(ctx, update, filter)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/invitations/storage.go b/pkg/invitations/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..047a33ea5ed0e84e064418f8774a8b852495efe9
--- /dev/null
+++ b/pkg/invitations/storage.go
@@ -0,0 +1,17 @@
+package invitations
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Storage interface {
+	Init(ctx context.Context) error
+	Reset(ctx context.Context) error
+
+	Create(ctx context.Context, create *Invitation) (invitation *Invitation, err error)
+	Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (invitations []*Invitation, total int, err error)
+	Delete(ctx context.Context, filter *Filter) (total int, err error)
+	Update(ctx context.Context, update *Invitation, filter *Filter) (updated, total int, err error)
+}
diff --git a/pkg/items/middleware/telemetry_middleware.go b/pkg/items/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..1e07b57ac5e0320c0962acd9d96b514901d88ea5
--- /dev/null
+++ b/pkg/items/middleware/telemetry_middleware.go
@@ -0,0 +1,465 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/items -i Items -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements items.Items interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	items.Items
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base items.Items, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Items:     base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Aggregate implements items.Items
+func (_d telemetryMiddleware) Aggregate(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregateOptions) (result map[string]interface{}, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Aggregate")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"filter":       filter,
+				"options":      options}, map[string]interface{}{
+				"result": result,
+				"err":    err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Aggregate(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+// AggregatePublished implements items.Items
+func (_d telemetryMiddleware) AggregatePublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.AggregatePublished")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"filter":       filter,
+				"options":      options}, map[string]interface{}{
+				"result": result,
+				"err":    err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.AggregatePublished(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+// Archive implements items.Items
+func (_d telemetryMiddleware) Archive(ctx context.Context, item *items.Item, options ...*items.ArchiveOptions) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Archive")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"item":    item,
+				"options": options}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Archive(ctx, item, options...)
+}
+
+// Create implements items.Items
+func (_d telemetryMiddleware) Create(ctx context.Context, item *items.Item, opts ...*items.CreateOptions) (created *items.Item, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":  ctx,
+				"item": item,
+				"opts": opts}, map[string]interface{}{
+				"created": created,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Create(ctx, item, opts...)
+}
+
+// Delete implements items.Items
+func (_d telemetryMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"itemId":       itemId,
+				"options":      options}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Delete(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+// Find implements items.Items
+func (_d telemetryMiddleware) Find(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindOptions) (items []*items.Item, total int, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Find")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"filter":       filter,
+				"options":      options}, map[string]interface{}{
+				"items": items,
+				"total": total,
+				"err":   err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Find(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+// FindArchived implements items.Items
+func (_d telemetryMiddleware) FindArchived(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindArchivedOptions) (items []*items.Item, total int, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.FindArchived")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"filter":       filter,
+				"options":      options}, map[string]interface{}{
+				"items": items,
+				"total": total,
+				"err":   err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.FindArchived(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+// FindPublished implements items.Items
+func (_d telemetryMiddleware) FindPublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, total int, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.FindPublished")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"filter":       filter,
+				"options":      options}, map[string]interface{}{
+				"items": items,
+				"total": total,
+				"err":   err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.FindPublished(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+// Get implements items.Items
+func (_d telemetryMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetOptions) (item *items.Item, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"itemId":       itemId,
+				"options":      options}, map[string]interface{}{
+				"item": item,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Get(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+// GetPublished implements items.Items
+func (_d telemetryMiddleware) GetPublished(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.GetPublished")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"itemId":       itemId,
+				"options":      options}, map[string]interface{}{
+				"item": item,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.GetPublished(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+// GetRevision implements items.Items
+func (_d telemetryMiddleware) GetRevision(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, revisionId string, options ...*items.GetRevisionOptions) (item *items.Item, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.GetRevision")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"itemId":       itemId,
+				"revisionId":   revisionId,
+				"options":      options}, map[string]interface{}{
+				"item": item,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.GetRevision(ctx, spaceId, envId, collectionId, itemId, revisionId, options...)
+}
+
+// Introspect implements items.Items
+func (_d telemetryMiddleware) Introspect(ctx context.Context, item *items.Item, opts ...*items.IntrospectOptions) (itm *items.Item, sch *schema.Schema, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Introspect")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":  ctx,
+				"item": item,
+				"opts": opts}, map[string]interface{}{
+				"itm": itm,
+				"sch": sch,
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Introspect(ctx, item, opts...)
+}
+
+// ListRevisions implements items.Items
+func (_d telemetryMiddleware) ListRevisions(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.ListRevisionsOptions) (items []*items.Item, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.ListRevisions")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"itemId":       itemId,
+				"options":      options}, map[string]interface{}{
+				"items": items,
+				"err":   err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.ListRevisions(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+// Publish implements items.Items
+func (_d telemetryMiddleware) Publish(ctx context.Context, item *items.Item, options ...*items.PublishOptions) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Publish")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"item":    item,
+				"options": options}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Publish(ctx, item, options...)
+}
+
+// Unarchive implements items.Items
+func (_d telemetryMiddleware) Unarchive(ctx context.Context, item *items.Item, options ...*items.UnarchiveOptions) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Unarchive")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"item":    item,
+				"options": options}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Unarchive(ctx, item, options...)
+}
+
+// Undelete implements items.Items
+func (_d telemetryMiddleware) Undelete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.UndeleteOptions) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Undelete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":          ctx,
+				"spaceId":      spaceId,
+				"envId":        envId,
+				"collectionId": collectionId,
+				"itemId":       itemId,
+				"options":      options}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Undelete(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+// Unpublish implements items.Items
+func (_d telemetryMiddleware) Unpublish(ctx context.Context, item *items.Item, options ...*items.UnpublishOptions) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Unpublish")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"item":    item,
+				"options": options}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Unpublish(ctx, item, options...)
+}
+
+// Update implements items.Items
+func (_d telemetryMiddleware) Update(ctx context.Context, item *items.Item, options ...*items.UpdateOptions) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Items.Update")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"item":    item,
+				"options": options}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Items.Update(ctx, item, options...)
+}
diff --git a/pkg/items/mocks/ItemObserver.go b/pkg/items/mocks/ItemObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..a72fb9b529eaf84c5da4c52fc2a62846f15cfb76
--- /dev/null
+++ b/pkg/items/mocks/ItemObserver.go
@@ -0,0 +1,25 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// ItemObserver is an autogenerated mock type for the ItemObserver type
+type ItemObserver struct {
+	mock.Mock
+}
+
+type mockConstructorTestingTNewItemObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewItemObserver creates a new instance of ItemObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewItemObserver(t mockConstructorTestingTNewItemObserver) *ItemObserver {
+	mock := &ItemObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/items/mocks/ItemReadObserver.go b/pkg/items/mocks/ItemReadObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..b14c8aafee42dabc44999993c56144763e4bd96d
--- /dev/null
+++ b/pkg/items/mocks/ItemReadObserver.go
@@ -0,0 +1,111 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	items "git.perx.ru/perxis/perxis-go/pkg/items"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// ItemReadObserver is an autogenerated mock type for the ItemReadObserver type
+type ItemReadObserver struct {
+	mock.Mock
+}
+
+// OnPostFind provides a mock function with given fields: ctx, _a1, total
+func (_m *ItemReadObserver) OnPostFind(ctx context.Context, _a1 []*items.Item, total int) ([]*items.Item, int, error) {
+	ret := _m.Called(ctx, _a1, total)
+
+	var r0 []*items.Item
+	if rf, ok := ret.Get(0).(func(context.Context, []*items.Item, int) []*items.Item); ok {
+		r0 = rf(ctx, _a1, total)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*items.Item)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, []*items.Item, int) int); ok {
+		r1 = rf(ctx, _a1, total)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, []*items.Item, int) error); ok {
+		r2 = rf(ctx, _a1, total)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// OnPostGet provides a mock function with given fields: ctx, item
+func (_m *ItemReadObserver) OnPostGet(ctx context.Context, item *items.Item) (*items.Item, error) {
+	ret := _m.Called(ctx, item)
+
+	var r0 *items.Item
+	if rf, ok := ret.Get(0).(func(context.Context, *items.Item) *items.Item); ok {
+		r0 = rf(ctx, item)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*items.Item)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *items.Item) error); ok {
+		r1 = rf(ctx, item)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// OnPreFind provides a mock function with given fields: ctx, spaceId, envId, collectionId, filter, options
+func (_m *ItemReadObserver) OnPreFind(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options *items.FindOptions) error {
+	ret := _m.Called(ctx, spaceId, envId, collectionId, filter, options)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, *items.Filter, *items.FindOptions) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId, filter, options)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// OnPreGet provides a mock function with given fields: ctx, spaceId, envId, collectionId, itemId
+func (_m *ItemReadObserver) OnPreGet(ctx context.Context, spaceId string, envId string, collectionId string, itemId string) error {
+	ret := _m.Called(ctx, spaceId, envId, collectionId, itemId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId, itemId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewItemReadObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewItemReadObserver creates a new instance of ItemReadObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewItemReadObserver(t mockConstructorTestingTNewItemReadObserver) *ItemReadObserver {
+	mock := &ItemReadObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/items/mocks/Storage.go b/pkg/items/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..9c502969ad292d0f99ad247394001ca0e8e29011
--- /dev/null
+++ b/pkg/items/mocks/Storage.go
@@ -0,0 +1,581 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	collections "git.perx.ru/perxis/perxis-go/pkg/collections"
+
+	items "git.perx.ru/perxis/perxis-go/pkg/items"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Aggregate provides a mock function with given fields: ctx, coll, filter, options
+func (_m *Storage) Aggregate(ctx context.Context, coll *collections.Collection, filter *items.Filter, options ...*items.AggregateOptions) (map[string]interface{}, error) {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, coll, filter)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 map[string]interface{}
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, *items.Filter, ...*items.AggregateOptions) map[string]interface{}); ok {
+		r0 = rf(ctx, coll, filter, options...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(map[string]interface{})
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection, *items.Filter, ...*items.AggregateOptions) error); ok {
+		r1 = rf(ctx, coll, filter, options...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// AggregatePublished provides a mock function with given fields: ctx, coll, filter, options
+func (_m *Storage) AggregatePublished(ctx context.Context, coll *collections.Collection, filter *items.Filter, options ...*items.AggregatePublishedOptions) (map[string]interface{}, error) {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, coll, filter)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 map[string]interface{}
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, *items.Filter, ...*items.AggregatePublishedOptions) map[string]interface{}); ok {
+		r0 = rf(ctx, coll, filter, options...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(map[string]interface{})
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection, *items.Filter, ...*items.AggregatePublishedOptions) error); ok {
+		r1 = rf(ctx, coll, filter, options...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Archive provides a mock function with given fields: ctx, archived, options
+func (_m *Storage) Archive(ctx context.Context, archived *items.Item, options ...*items.ArchiveOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, archived)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *items.Item, ...*items.ArchiveOptions) error); ok {
+		r0 = rf(ctx, archived, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// ChangeRevisionsItemID provides a mock function with given fields: ctx, spaceId, envId, collectionId, itemId, newItemId
+func (_m *Storage) ChangeRevisionsItemID(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, newItemId string) error {
+	ret := _m.Called(ctx, spaceId, envId, collectionId, itemId, newItemId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId, itemId, newItemId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Copy provides a mock function with given fields: ctx, src, dst, itemSets
+func (_m *Storage) Copy(ctx context.Context, src *collections.Collection, dst *collections.Collection, itemSets ...string) error {
+	_va := make([]interface{}, len(itemSets))
+	for _i := range itemSets {
+		_va[_i] = itemSets[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, src, dst)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, *collections.Collection, ...string) error); ok {
+		r0 = rf(ctx, src, dst, itemSets...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Create provides a mock function with given fields: ctx, coll, item, options
+func (_m *Storage) Create(ctx context.Context, coll *collections.Collection, item *items.Item, options ...*items.CreateOptions) (*items.Item, error) {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, coll, item)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 *items.Item
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, *items.Item, ...*items.CreateOptions) *items.Item); ok {
+		r0 = rf(ctx, coll, item, options...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*items.Item)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection, *items.Item, ...*items.CreateOptions) error); ok {
+		r1 = rf(ctx, coll, item, options...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// CreateRevision provides a mock function with given fields: ctx, spaceId, envId, collectionId, itemId
+func (_m *Storage) CreateRevision(ctx context.Context, spaceId string, envId string, collectionId string, itemId string) error {
+	ret := _m.Called(ctx, spaceId, envId, collectionId, itemId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId, itemId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Find provides a mock function with given fields: ctx, coll, filter, opts
+func (_m *Storage) Find(ctx context.Context, coll *collections.Collection, filter *items.Filter, opts ...*items.FindOptions) ([]*items.Item, int, error) {
+	_va := make([]interface{}, len(opts))
+	for _i := range opts {
+		_va[_i] = opts[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, coll, filter)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 []*items.Item
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, *items.Filter, ...*items.FindOptions) []*items.Item); ok {
+		r0 = rf(ctx, coll, filter, opts...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*items.Item)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection, *items.Filter, ...*items.FindOptions) int); ok {
+		r1 = rf(ctx, coll, filter, opts...)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *collections.Collection, *items.Filter, ...*items.FindOptions) error); ok {
+		r2 = rf(ctx, coll, filter, opts...)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// FindArchived provides a mock function with given fields: ctx, coll, filter, opts
+func (_m *Storage) FindArchived(ctx context.Context, coll *collections.Collection, filter *items.Filter, opts ...*items.FindArchivedOptions) ([]*items.Item, int, error) {
+	_va := make([]interface{}, len(opts))
+	for _i := range opts {
+		_va[_i] = opts[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, coll, filter)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 []*items.Item
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, *items.Filter, ...*items.FindArchivedOptions) []*items.Item); ok {
+		r0 = rf(ctx, coll, filter, opts...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*items.Item)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection, *items.Filter, ...*items.FindArchivedOptions) int); ok {
+		r1 = rf(ctx, coll, filter, opts...)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *collections.Collection, *items.Filter, ...*items.FindArchivedOptions) error); ok {
+		r2 = rf(ctx, coll, filter, opts...)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// FindPublished provides a mock function with given fields: ctx, coll, filter, opts
+func (_m *Storage) FindPublished(ctx context.Context, coll *collections.Collection, filter *items.Filter, opts ...*items.FindPublishedOptions) ([]*items.Item, int, error) {
+	_va := make([]interface{}, len(opts))
+	for _i := range opts {
+		_va[_i] = opts[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, coll, filter)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 []*items.Item
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, *items.Filter, ...*items.FindPublishedOptions) []*items.Item); ok {
+		r0 = rf(ctx, coll, filter, opts...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*items.Item)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection, *items.Filter, ...*items.FindPublishedOptions) int); ok {
+		r1 = rf(ctx, coll, filter, opts...)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *collections.Collection, *items.Filter, ...*items.FindPublishedOptions) error); ok {
+		r2 = rf(ctx, coll, filter, opts...)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// GetRevision provides a mock function with given fields: ctx, coll, itemId, revisionId, options
+func (_m *Storage) GetRevision(ctx context.Context, coll *collections.Collection, itemId string, revisionId string, options ...*items.GetRevisionOptions) (*items.Item, error) {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, coll, itemId, revisionId)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 *items.Item
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, string, string, ...*items.GetRevisionOptions) *items.Item); ok {
+		r0 = rf(ctx, coll, itemId, revisionId, options...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*items.Item)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection, string, string, ...*items.GetRevisionOptions) error); ok {
+		r1 = rf(ctx, coll, itemId, revisionId, options...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Init provides a mock function with given fields: ctx, collection
+func (_m *Storage) Init(ctx context.Context, collection *collections.Collection) error {
+	ret := _m.Called(ctx, collection)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection) error); ok {
+		r0 = rf(ctx, collection)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// ListRevisions provides a mock function with given fields: ctx, coll, itemId, options
+func (_m *Storage) ListRevisions(ctx context.Context, coll *collections.Collection, itemId string, options ...*items.ListRevisionsOptions) ([]*items.Item, error) {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, coll, itemId)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 []*items.Item
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, string, ...*items.ListRevisionsOptions) []*items.Item); ok {
+		r0 = rf(ctx, coll, itemId, options...)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*items.Item)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *collections.Collection, string, ...*items.ListRevisionsOptions) error); ok {
+		r1 = rf(ctx, coll, itemId, options...)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Publish provides a mock function with given fields: ctx, published, options
+func (_m *Storage) Publish(ctx context.Context, published *items.Item, options ...*items.PublishOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, published)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *items.Item, ...*items.PublishOptions) error); ok {
+		r0 = rf(ctx, published, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// RemoveArchived provides a mock function with given fields: ctx, spaceId, envId, collectionId, itemId, options
+func (_m *Storage) RemoveArchived(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, spaceId, envId, collectionId, itemId)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, ...*items.DeleteOptions) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId, itemId, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// RemoveItems provides a mock function with given fields: ctx, spaceId, envId, collectionId, itemId, options
+func (_m *Storage) RemoveItems(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, spaceId, envId, collectionId, itemId)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, ...*items.DeleteOptions) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId, itemId, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// RemovePublished provides a mock function with given fields: ctx, spaceId, envId, collectionId, itemId, options
+func (_m *Storage) RemovePublished(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, spaceId, envId, collectionId, itemId)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, ...*items.DeleteOptions) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId, itemId, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// RemoveRevision provides a mock function with given fields: ctx, spaceId, envId, collectionId, revision, options
+func (_m *Storage) RemoveRevision(ctx context.Context, spaceId string, envId string, collectionId string, revision string, options ...*items.DeleteOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, spaceId, envId, collectionId, revision)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, ...*items.DeleteOptions) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId, revision, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// RemoveRevisions provides a mock function with given fields: ctx, spaceId, envId, collectionId, itemId, options
+func (_m *Storage) RemoveRevisions(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, spaceId, envId, collectionId, itemId)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, ...*items.DeleteOptions) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId, itemId, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Reset provides a mock function with given fields: ctx, spaceId, envId, collectionId
+func (_m *Storage) Reset(ctx context.Context, spaceId string, envId string, collectionId string) error {
+	ret := _m.Called(ctx, spaceId, envId, collectionId)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
+		r0 = rf(ctx, spaceId, envId, collectionId)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Unarchive provides a mock function with given fields: ctx, unarchived, options
+func (_m *Storage) Unarchive(ctx context.Context, unarchived *items.Item, options ...*items.UnarchiveOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, unarchived)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *items.Item, ...*items.UnarchiveOptions) error); ok {
+		r0 = rf(ctx, unarchived, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Unpublish provides a mock function with given fields: ctx, unpublished, options
+func (_m *Storage) Unpublish(ctx context.Context, unpublished *items.Item, options ...*items.UnpublishOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, unpublished)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *items.Item, ...*items.UnpublishOptions) error); ok {
+		r0 = rf(ctx, unpublished, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, coll, item, options
+func (_m *Storage) Update(ctx context.Context, coll *collections.Collection, item *items.Item, options ...*items.UpdateOptions) error {
+	_va := make([]interface{}, len(options))
+	for _i := range options {
+		_va[_i] = options[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, coll, item)
+	_ca = append(_ca, _va...)
+	ret := _m.Called(_ca...)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *collections.Collection, *items.Item, ...*items.UpdateOptions) error); ok {
+		r0 = rf(ctx, coll, item, options...)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/items/observer.go b/pkg/items/observer.go
new file mode 100644
index 0000000000000000000000000000000000000000..6eb7194df4dbecd7f4b9e4aa4a86452785b5d801
--- /dev/null
+++ b/pkg/items/observer.go
@@ -0,0 +1,14 @@
+package items
+
+import (
+	"context"
+)
+
+type ItemObserver interface{}
+
+type ItemReadObserver interface {
+	OnPreGet(ctx context.Context, spaceId, envId, collectionId, itemId string) error
+	OnPostGet(ctx context.Context, item *Item) (*Item, error)
+	OnPreFind(ctx context.Context, spaceId, envId, collectionId string, filter *Filter, options *FindOptions) error
+	OnPostFind(ctx context.Context, items []*Item, total int) ([]*Item, int, error)
+}
diff --git a/pkg/items/storage.go b/pkg/items/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..64548a009369fad6e74530e2ae5f52dd013e7997
--- /dev/null
+++ b/pkg/items/storage.go
@@ -0,0 +1,92 @@
+package items
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+)
+
+type Storage interface {
+	// Items
+	// ---------------------------------------------------------------------------------------------
+	// Все пользовательские коллекции Perxis хранятся вместе и данные могут быть выбраны одновременно
+	// из разных коллекций.
+	//
+	// Для работы с Items используются 3 коллекции MongoDB между которыми записи переносятся в зависимости
+	// от их статуса.
+	// - 'items' - рабочие записи с которыми работает API управления контентом. В коллекции находятся
+	//		только текущие ревизии записи
+	// - 'revisions' - все ревизии записи за исключением текущей. Ревизии записей переносятся в данную
+	//		коллекцию при создании новой ревизии записи. Поиск данных по коллекции не производится
+	// - 'published' - опубликованные записи, в данную коллекцию копируются записи из `items` при
+	//		публикации. В коллекции находится только одна опубликованная ревизия. Коллекция обеспечивает
+	//		быстрый поиск, поэтому для данных строятся Spare-индексы с использованием Схем
+	// - 'archived' - архивные записи, в данную коллекцию записи переносятся при архивировании из `items`.
+	//		Копии записей из `published` при этом удаляются.
+	// ---------------------------------------------------------------------------------------------
+
+	// Create - создать новую запись
+	Create(ctx context.Context, coll *collections.Collection, item *Item, options ...*CreateOptions) (*Item, error)
+
+	// CreateRevision - перенести ревизию в коллекцию Revisions
+	CreateRevision(ctx context.Context, spaceId, envId, collectionId, itemId string) error
+
+	// Update - Обновление текущей ревизии или создание новой, если в опциях передан флаг `ReplacePublishedRevision`
+	Update(ctx context.Context, coll *collections.Collection, item *Item, options ...*UpdateOptions) error
+
+	// Find - поиск записей по рабочим записям, коллекция 'items'
+	Find(ctx context.Context, coll *collections.Collection, filter *Filter, opts ...*FindOptions) ([]*Item, int, error)
+
+	// GetRevision - поиск одной ревизии одной записи
+	GetRevision(ctx context.Context, coll *collections.Collection, itemId, revisionId string, options ...*GetRevisionOptions) (*Item, error)
+
+	// ListRevisions - поиск всех ревизий одной записи
+	ListRevisions(ctx context.Context, coll *collections.Collection, itemId string, options ...*ListRevisionsOptions) ([]*Item, error)
+
+	// ChangeRevisionsItemID - заменить ID элемента у его ревизий
+	ChangeRevisionsItemID(ctx context.Context, spaceId, envId, collectionId, itemId, newItemId string) error
+
+	// Publish - опубликовать запись
+	Publish(ctx context.Context, published *Item, options ...*PublishOptions) error
+
+	// Unpublish - отменить публикацию записи
+	Unpublish(ctx context.Context, unpublished *Item, options ...*UnpublishOptions) error
+
+	// FindPublished - поиск по опубликованным записям, коллекция 'items_published'
+	FindPublished(ctx context.Context, coll *collections.Collection, filter *Filter, opts ...*FindPublishedOptions) ([]*Item, int, error)
+
+	// Archive - архивация записи
+	Archive(ctx context.Context, archived *Item, options ...*ArchiveOptions) error
+
+	// Unarchive - разархивация записи
+	Unarchive(ctx context.Context, unarchived *Item, options ...*UnarchiveOptions) error
+
+	// FindArchived - поиск по архивированным записям, коллекция 'items_archived'
+	FindArchived(ctx context.Context, coll *collections.Collection, filter *Filter, opts ...*FindArchivedOptions) ([]*Item, int, error)
+
+	// RemoveItems - удаление записи из коллекций Items
+	RemoveItems(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*DeleteOptions) error
+
+	// RemovePublished - удаление записи из коллекций Published
+	RemovePublished(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*DeleteOptions) error
+
+	// RemoveRevisions - удаление записи из всех ревизий элемента
+	RemoveRevisions(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*DeleteOptions) error
+
+	// RemoveRevision - удаление конкретной ревизии из Revisions
+	RemoveRevision(ctx context.Context, spaceId, envId, collectionId, revision string, options ...*DeleteOptions) error
+
+	// RemoveArchived - удаление записи из архива
+	RemoveArchived(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*DeleteOptions) error
+
+	// Copy - копирование записей
+	Copy(ctx context.Context, src, dst *collections.Collection, itemSets ...string) error
+
+	Reset(ctx context.Context, spaceId, envId, collectionId string) error
+	Init(ctx context.Context, collection *collections.Collection) error
+
+	// Aggregate выполняет агрегацию данных
+	Aggregate(ctx context.Context, coll *collections.Collection, filter *Filter, options ...*AggregateOptions) (result map[string]interface{}, err error)
+	// AggregatePublished выполняет агрегацию опубликованных данных
+	AggregatePublished(ctx context.Context, coll *collections.Collection, filter *Filter, options ...*AggregatePublishedOptions) (result map[string]interface{}, err error)
+}
diff --git a/pkg/locales/middleware/telemetry_middleware.go b/pkg/locales/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..3e335e19574e238a02a69dc19f373c62e7fc65a2
--- /dev/null
+++ b/pkg/locales/middleware/telemetry_middleware.go
@@ -0,0 +1,100 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/locales -i Locales -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/locales"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements locales.Locales interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	locales.Locales
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base locales.Locales, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Locales:   base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Create implements locales.Locales
+func (_d telemetryMiddleware) Create(ctx context.Context, locale *locales.Locale) (created *locales.Locale, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Locales.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"locale": locale}, map[string]interface{}{
+				"created": created,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Locales.Create(ctx, locale)
+}
+
+// Delete implements locales.Locales
+func (_d telemetryMiddleware) Delete(ctx context.Context, spaceId string, localeId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Locales.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":      ctx,
+				"spaceId":  spaceId,
+				"localeId": localeId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Locales.Delete(ctx, spaceId, localeId)
+}
+
+// List implements locales.Locales
+func (_d telemetryMiddleware) List(ctx context.Context, spaceId string) (locales []*locales.Locale, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Locales.List")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId}, map[string]interface{}{
+				"locales": locales,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Locales.List(ctx, spaceId)
+}
diff --git a/pkg/locales/mocks/Storage.go b/pkg/locales/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..d37bd81cf7011e2498768c76102bb71a689eaf84
--- /dev/null
+++ b/pkg/locales/mocks/Storage.go
@@ -0,0 +1,120 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	locales "git.perx.ru/perxis/perxis-go/pkg/locales"
+	mock "github.com/stretchr/testify/mock"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, locale
+func (_m *Storage) Create(ctx context.Context, locale *locales.Locale) (*locales.Locale, error) {
+	ret := _m.Called(ctx, locale)
+
+	var r0 *locales.Locale
+	if rf, ok := ret.Get(0).(func(context.Context, *locales.Locale) *locales.Locale); ok {
+		r0 = rf(ctx, locale)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*locales.Locale)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *locales.Locale) error); ok {
+		r1 = rf(ctx, locale)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, spaceID, filter
+func (_m *Storage) Delete(ctx context.Context, spaceID string, filter *locales.Filter) (int, error) {
+	ret := _m.Called(ctx, spaceID, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, string, *locales.Filter) int); ok {
+		r0 = rf(ctx, spaceID, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, string, *locales.Filter) error); ok {
+		r1 = rf(ctx, spaceID, filter)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Find provides a mock function with given fields: ctx, spaceID, filter, opts
+func (_m *Storage) Find(ctx context.Context, spaceID string, filter *locales.Filter, opts *options.FindOptions) ([]*locales.Locale, int, error) {
+	ret := _m.Called(ctx, spaceID, filter, opts)
+
+	var r0 []*locales.Locale
+	if rf, ok := ret.Get(0).(func(context.Context, string, *locales.Filter, *options.FindOptions) []*locales.Locale); ok {
+		r0 = rf(ctx, spaceID, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*locales.Locale)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, string, *locales.Filter, *options.FindOptions) int); ok {
+		r1 = rf(ctx, spaceID, filter, opts)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, string, *locales.Filter, *options.FindOptions) error); ok {
+		r2 = rf(ctx, spaceID, filter, opts)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// Reset provides a mock function with given fields: ctx, spaceID
+func (_m *Storage) Reset(ctx context.Context, spaceID string) error {
+	ret := _m.Called(ctx, spaceID)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, spaceID)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/locales/storage.go b/pkg/locales/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..c345706c5925119cf3eb84a07f91e57ff764f46f
--- /dev/null
+++ b/pkg/locales/storage.go
@@ -0,0 +1,20 @@
+package locales
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Storage interface {
+	Reset(ctx context.Context, spaceID string) error
+
+	Create(ctx context.Context, locale *Locale) (created *Locale, err error)
+	Find(ctx context.Context, spaceID string, filter *Filter, opts *options.FindOptions) (locales []*Locale, total int, err error)
+	Delete(ctx context.Context, spaceID string, filter *Filter) (total int, err error)
+}
+
+type Filter struct {
+	ID   []string
+	Name []string
+}
diff --git a/pkg/members/middleware/telemetry_middleware.go b/pkg/members/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..4d13a5d794a57fd9c470bae42bbfed4fe81743cc
--- /dev/null
+++ b/pkg/members/middleware/telemetry_middleware.go
@@ -0,0 +1,164 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/members -i Members -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements members.Members interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	members.Members
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base members.Members, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Members:   base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Get implements members.Members
+func (_d telemetryMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Members.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"orgId":  orgId,
+				"userId": userId}, map[string]interface{}{
+				"role": role,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Members.Get(ctx, orgId, userId)
+}
+
+// ListMembers implements members.Members
+func (_d telemetryMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Members.ListMembers")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":   ctx,
+				"orgId": orgId}, map[string]interface{}{
+				"members": members,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Members.ListMembers(ctx, orgId)
+}
+
+// ListOrganizations implements members.Members
+func (_d telemetryMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Members.ListOrganizations")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"userId": userId}, map[string]interface{}{
+				"organizations": organizations,
+				"err":           err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Members.ListOrganizations(ctx, userId)
+}
+
+// Remove implements members.Members
+func (_d telemetryMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Members.Remove")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"orgId":  orgId,
+				"userId": userId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Members.Remove(ctx, orgId, userId)
+}
+
+// RemoveAll implements members.Members
+func (_d telemetryMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Members.RemoveAll")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":   ctx,
+				"orgId": orgId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Members.RemoveAll(ctx, orgId)
+}
+
+// Set implements members.Members
+func (_d telemetryMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Members.Set")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"orgId":  orgId,
+				"userId": userId,
+				"role":   role}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Members.Set(ctx, orgId, userId, role)
+}
diff --git a/pkg/members/mocks/Middleware.go b/pkg/members/mocks/Middleware.go
index 22b7a5dd3f3a42862b9899e6554b049c4595835c..b953cf79d8239ef8da125e809bd29e1cf0a649f0 100644
--- a/pkg/members/mocks/Middleware.go
+++ b/pkg/members/mocks/Middleware.go
@@ -3,8 +3,7 @@
 package mocks
 
 import (
-	members "git.perx.ru/perxis/perxis-go/pkg/members"
-
+	observer "git.perx.ru/perxis/perxis-go/pkg/members/observer"
 	mock "github.com/stretchr/testify/mock"
 )
 
@@ -14,15 +13,15 @@ type Middleware struct {
 }
 
 // Execute provides a mock function with given fields: _a0
-func (_m *Middleware) Execute(_a0 members.Members) members.Members {
+func (_m *Middleware) Execute(_a0 observer.Observer) observer.Observer {
 	ret := _m.Called(_a0)
 
-	var r0 members.Members
-	if rf, ok := ret.Get(0).(func(members.Members) members.Members); ok {
+	var r0 observer.Observer
+	if rf, ok := ret.Get(0).(func(observer.Observer) observer.Observer); ok {
 		r0 = rf(_a0)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(members.Members)
+			r0 = ret.Get(0).(observer.Observer)
 		}
 	}
 
diff --git a/pkg/members/mocks/Storage.go b/pkg/members/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..1527b6e09fce71d7b35d84d04613d2dcbfa96249
--- /dev/null
+++ b/pkg/members/mocks/Storage.go
@@ -0,0 +1,118 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	members "git.perx.ru/perxis/perxis-go/pkg/members"
+	mock "github.com/stretchr/testify/mock"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Find provides a mock function with given fields: ctx, filter, opts
+func (_m *Storage) Find(ctx context.Context, filter *members.Filter, opts *options.FindOptions) ([]*members.Member, error) {
+	ret := _m.Called(ctx, filter, opts)
+
+	var r0 []*members.Member
+	if rf, ok := ret.Get(0).(func(context.Context, *members.Filter, *options.FindOptions) []*members.Member); ok {
+		r0 = rf(ctx, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*members.Member)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *members.Filter, *options.FindOptions) error); ok {
+		r1 = rf(ctx, filter, opts)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Init provides a mock function with given fields: ctx
+func (_m *Storage) Init(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Remove provides a mock function with given fields: ctx, filter
+func (_m *Storage) Remove(ctx context.Context, filter *members.Filter) (int, error) {
+	ret := _m.Called(ctx, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *members.Filter) int); ok {
+		r0 = rf(ctx, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *members.Filter) error); ok {
+		r1 = rf(ctx, filter)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Reset provides a mock function with given fields: ctx
+func (_m *Storage) Reset(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Set provides a mock function with given fields: ctx, orgID, userID, role
+func (_m *Storage) Set(ctx context.Context, orgID string, userID string, role members.Role) error {
+	ret := _m.Called(ctx, orgID, userID, role)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, members.Role) error); ok {
+		r0 = rf(ctx, orgID, userID, role)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/members/storage.go b/pkg/members/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec6daea171e0a69521770ed362a883a5aa6166f5
--- /dev/null
+++ b/pkg/members/storage.go
@@ -0,0 +1,22 @@
+package members
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Storage interface {
+	Reset(ctx context.Context) error
+	Init(ctx context.Context) error
+
+	Set(ctx context.Context, orgID, userID string, role Role) (err error)
+	Remove(ctx context.Context, filter *Filter) (total int, err error)
+	Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (memberships []*Member, err error)
+}
+
+type Filter struct {
+	OrgID  string
+	UserID string
+	Role   Role
+}
diff --git a/pkg/organizations/middleware/telemetry_middleware.go b/pkg/organizations/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..6589ed10ba1ee502c6620f8cbef20ed6da22bb29
--- /dev/null
+++ b/pkg/organizations/middleware/telemetry_middleware.go
@@ -0,0 +1,143 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/organizations -i Organizations -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"git.perx.ru/perxis/perxis-go/pkg/organizations"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements organizations.Organizations interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	organizations.Organizations
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base organizations.Organizations, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Organizations: base,
+		_instance:     instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Create implements organizations.Organizations
+func (_d telemetryMiddleware) Create(ctx context.Context, org *organizations.Organization) (created *organizations.Organization, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Organizations.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx": ctx,
+				"org": org}, map[string]interface{}{
+				"created": created,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Organizations.Create(ctx, org)
+}
+
+// Delete implements organizations.Organizations
+func (_d telemetryMiddleware) Delete(ctx context.Context, orgId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Organizations.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":   ctx,
+				"orgId": orgId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Organizations.Delete(ctx, orgId)
+}
+
+// Find implements organizations.Organizations
+func (_d telemetryMiddleware) Find(ctx context.Context, filter *organizations.Filter, opts *options.FindOptions) (orgs []*organizations.Organization, total int, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Organizations.Find")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"filter": filter,
+				"opts":   opts}, map[string]interface{}{
+				"orgs":  orgs,
+				"total": total,
+				"err":   err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Organizations.Find(ctx, filter, opts)
+}
+
+// Get implements organizations.Organizations
+func (_d telemetryMiddleware) Get(ctx context.Context, orgId string) (org *organizations.Organization, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Organizations.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":   ctx,
+				"orgId": orgId}, map[string]interface{}{
+				"org": org,
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Organizations.Get(ctx, orgId)
+}
+
+// Update implements organizations.Organizations
+func (_d telemetryMiddleware) Update(ctx context.Context, org *organizations.Organization) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Organizations.Update")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx": ctx,
+				"org": org}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Organizations.Update(ctx, org)
+}
diff --git a/pkg/organizations/mocks/Storage.go b/pkg/organizations/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..6b2b7176ff452b0e5935b352ccc3ee112182eb0b
--- /dev/null
+++ b/pkg/organizations/mocks/Storage.go
@@ -0,0 +1,162 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	mock "github.com/stretchr/testify/mock"
+
+	organizations "git.perx.ru/perxis/perxis-go/pkg/organizations"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, org
+func (_m *Storage) Create(ctx context.Context, org *organizations.Organization) (*organizations.Organization, error) {
+	ret := _m.Called(ctx, org)
+
+	var r0 *organizations.Organization
+	if rf, ok := ret.Get(0).(func(context.Context, *organizations.Organization) *organizations.Organization); ok {
+		r0 = rf(ctx, org)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*organizations.Organization)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *organizations.Organization) error); ok {
+		r1 = rf(ctx, org)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, filter
+func (_m *Storage) Delete(ctx context.Context, filter *organizations.Filter) (int, error) {
+	ret := _m.Called(ctx, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *organizations.Filter) int); ok {
+		r0 = rf(ctx, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *organizations.Filter) error); ok {
+		r1 = rf(ctx, filter)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Find provides a mock function with given fields: ctx, filter, opts
+func (_m *Storage) Find(ctx context.Context, filter *organizations.Filter, opts *options.FindOptions) ([]*organizations.Organization, int, error) {
+	ret := _m.Called(ctx, filter, opts)
+
+	var r0 []*organizations.Organization
+	if rf, ok := ret.Get(0).(func(context.Context, *organizations.Filter, *options.FindOptions) []*organizations.Organization); ok {
+		r0 = rf(ctx, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*organizations.Organization)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *organizations.Filter, *options.FindOptions) int); ok {
+		r1 = rf(ctx, filter, opts)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *organizations.Filter, *options.FindOptions) error); ok {
+		r2 = rf(ctx, filter, opts)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// Init provides a mock function with given fields: ctx
+func (_m *Storage) Init(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Reset provides a mock function with given fields: ctx
+func (_m *Storage) Reset(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, update, filter
+func (_m *Storage) Update(ctx context.Context, update *organizations.Organization, filter *organizations.Filter) (int, int, error) {
+	ret := _m.Called(ctx, update, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *organizations.Organization, *organizations.Filter) int); ok {
+		r0 = rf(ctx, update, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *organizations.Organization, *organizations.Filter) int); ok {
+		r1 = rf(ctx, update, filter)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *organizations.Organization, *organizations.Filter) error); ok {
+		r2 = rf(ctx, update, filter)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/organizations/storage.go b/pkg/organizations/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..1e628303e3d36c8717b3f2214578a569cb2efc07
--- /dev/null
+++ b/pkg/organizations/storage.go
@@ -0,0 +1,17 @@
+package organizations
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Storage interface {
+	Create(ctx context.Context, org *Organization) (created *Organization, err error)
+	Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (orgs []*Organization, total int, err error)
+	Delete(ctx context.Context, filter *Filter) (total int, err error)
+	Update(ctx context.Context, update *Organization, filter *Filter) (updated, total int, err error)
+
+	Reset(ctx context.Context) error
+	Init(ctx context.Context) error
+}
diff --git a/pkg/roles/middleware/telemetry_middleware.go b/pkg/roles/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..fe487489348a421c611c16b24c8fe689908dcaa8
--- /dev/null
+++ b/pkg/roles/middleware/telemetry_middleware.go
@@ -0,0 +1,142 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/roles -i Roles -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements roles.Roles interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	roles.Roles
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base roles.Roles, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Roles:     base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Create implements roles.Roles
+func (_d telemetryMiddleware) Create(ctx context.Context, role *roles.Role) (created *roles.Role, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Roles.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":  ctx,
+				"role": role}, map[string]interface{}{
+				"created": created,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Roles.Create(ctx, role)
+}
+
+// Delete implements roles.Roles
+func (_d telemetryMiddleware) Delete(ctx context.Context, spaceId string, roleId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Roles.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"roleId":  roleId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Roles.Delete(ctx, spaceId, roleId)
+}
+
+// Get implements roles.Roles
+func (_d telemetryMiddleware) Get(ctx context.Context, spaceId string, roleId string) (role *roles.Role, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Roles.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"roleId":  roleId}, map[string]interface{}{
+				"role": role,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Roles.Get(ctx, spaceId, roleId)
+}
+
+// List implements roles.Roles
+func (_d telemetryMiddleware) List(ctx context.Context, spaceId string) (roles []*roles.Role, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Roles.List")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId}, map[string]interface{}{
+				"roles": roles,
+				"err":   err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Roles.List(ctx, spaceId)
+}
+
+// Update implements roles.Roles
+func (_d telemetryMiddleware) Update(ctx context.Context, role *roles.Role) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Roles.Update")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":  ctx,
+				"role": role}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Roles.Update(ctx, role)
+}
diff --git a/pkg/roles/mocks/Storage.go b/pkg/roles/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..d2d525a19e2c10096a0a1222122e302ad5ea0ce7
--- /dev/null
+++ b/pkg/roles/mocks/Storage.go
@@ -0,0 +1,141 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	mock "github.com/stretchr/testify/mock"
+
+	roles "git.perx.ru/perxis/perxis-go/pkg/roles"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, role
+func (_m *Storage) Create(ctx context.Context, role *roles.Role) (*roles.Role, error) {
+	ret := _m.Called(ctx, role)
+
+	var r0 *roles.Role
+	if rf, ok := ret.Get(0).(func(context.Context, *roles.Role) *roles.Role); ok {
+		r0 = rf(ctx, role)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*roles.Role)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *roles.Role) error); ok {
+		r1 = rf(ctx, role)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, spaceID, roleID
+func (_m *Storage) Delete(ctx context.Context, spaceID string, roleID string) error {
+	ret := _m.Called(ctx, spaceID, roleID)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
+		r0 = rf(ctx, spaceID, roleID)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Find provides a mock function with given fields: ctx, spaceID, f, opts
+func (_m *Storage) Find(ctx context.Context, spaceID string, f *roles.Filter, opts *options.FindOptions) ([]*roles.Role, int, error) {
+	ret := _m.Called(ctx, spaceID, f, opts)
+
+	var r0 []*roles.Role
+	if rf, ok := ret.Get(0).(func(context.Context, string, *roles.Filter, *options.FindOptions) []*roles.Role); ok {
+		r0 = rf(ctx, spaceID, f, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*roles.Role)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, string, *roles.Filter, *options.FindOptions) int); ok {
+		r1 = rf(ctx, spaceID, f, opts)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, string, *roles.Filter, *options.FindOptions) error); ok {
+		r2 = rf(ctx, spaceID, f, opts)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// Reset provides a mock function with given fields: ctx, spaceID
+func (_m *Storage) Reset(ctx context.Context, spaceID string) error {
+	ret := _m.Called(ctx, spaceID)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
+		r0 = rf(ctx, spaceID)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, role
+func (_m *Storage) Update(ctx context.Context, role *roles.Role) (int, int, error) {
+	ret := _m.Called(ctx, role)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *roles.Role) int); ok {
+		r0 = rf(ctx, role)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *roles.Role) int); ok {
+		r1 = rf(ctx, role)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *roles.Role) error); ok {
+		r2 = rf(ctx, role)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/roles/storage.go b/pkg/roles/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..91b76a5e03c6f292cf7d368d19603ac1c1c36595
--- /dev/null
+++ b/pkg/roles/storage.go
@@ -0,0 +1,21 @@
+package roles
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Storage interface {
+	Create(ctx context.Context, role *Role) (created *Role, err error)
+	Find(ctx context.Context, spaceID string, f *Filter, opts *options.FindOptions) ([]*Role, int, error)
+	Update(ctx context.Context, role *Role) (updated, total int, err error)
+	Delete(ctx context.Context, spaceID, roleID string) (err error)
+
+	Reset(ctx context.Context, spaceID string) error
+}
+
+type Filter struct {
+	ID   string
+	Name string
+}
diff --git a/pkg/spaces/middleware/telemetry_middleware.go b/pkg/spaces/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..2ef9f21e58095bdbfbfdc1216f6bdd565c7089e8
--- /dev/null
+++ b/pkg/spaces/middleware/telemetry_middleware.go
@@ -0,0 +1,161 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/spaces -i Spaces -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements spaces.Spaces interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	spaces.Spaces
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base spaces.Spaces, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Spaces:    base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Create implements spaces.Spaces
+func (_d telemetryMiddleware) Create(ctx context.Context, space *spaces.Space) (created *spaces.Space, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Spaces.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":   ctx,
+				"space": space}, map[string]interface{}{
+				"created": created,
+				"err":     err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Spaces.Create(ctx, space)
+}
+
+// Delete implements spaces.Spaces
+func (_d telemetryMiddleware) Delete(ctx context.Context, spaceId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Spaces.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Spaces.Delete(ctx, spaceId)
+}
+
+// Get implements spaces.Spaces
+func (_d telemetryMiddleware) Get(ctx context.Context, spaceId string) (space *spaces.Space, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Spaces.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId}, map[string]interface{}{
+				"space": space,
+				"err":   err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Spaces.Get(ctx, spaceId)
+}
+
+// List implements spaces.Spaces
+func (_d telemetryMiddleware) List(ctx context.Context, orgId string) (spaces []*spaces.Space, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Spaces.List")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":   ctx,
+				"orgId": orgId}, map[string]interface{}{
+				"spaces": spaces,
+				"err":    err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Spaces.List(ctx, orgId)
+}
+
+// Update implements spaces.Spaces
+func (_d telemetryMiddleware) Update(ctx context.Context, space *spaces.Space) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Spaces.Update")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":   ctx,
+				"space": space}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Spaces.Update(ctx, space)
+}
+
+// UpdateConfig implements spaces.Spaces
+func (_d telemetryMiddleware) UpdateConfig(ctx context.Context, spaceId string, config *spaces.Config) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Spaces.UpdateConfig")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"spaceId": spaceId,
+				"config":  config}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Spaces.UpdateConfig(ctx, spaceId, config)
+}
diff --git a/pkg/spaces/mocks/SpaceCreatedObserver.go b/pkg/spaces/mocks/SpaceCreatedObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..a85eced793564cc3fc27304881486329041c3d75
--- /dev/null
+++ b/pkg/spaces/mocks/SpaceCreatedObserver.go
@@ -0,0 +1,44 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	spaces "git.perx.ru/perxis/perxis-go/pkg/spaces"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// SpaceCreatedObserver is an autogenerated mock type for the SpaceCreatedObserver type
+type SpaceCreatedObserver struct {
+	mock.Mock
+}
+
+// OnSpaceCreated provides a mock function with given fields: ctx, space
+func (_m *SpaceCreatedObserver) OnSpaceCreated(ctx context.Context, space *spaces.Space) error {
+	ret := _m.Called(ctx, space)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Space) error); ok {
+		r0 = rf(ctx, space)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewSpaceCreatedObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewSpaceCreatedObserver creates a new instance of SpaceCreatedObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewSpaceCreatedObserver(t mockConstructorTestingTNewSpaceCreatedObserver) *SpaceCreatedObserver {
+	mock := &SpaceCreatedObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/spaces/mocks/SpaceDeletedObserver.go b/pkg/spaces/mocks/SpaceDeletedObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..450173b03b0007c86b4dffee69a0c36522f9ce28
--- /dev/null
+++ b/pkg/spaces/mocks/SpaceDeletedObserver.go
@@ -0,0 +1,44 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	spaces "git.perx.ru/perxis/perxis-go/pkg/spaces"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// SpaceDeletedObserver is an autogenerated mock type for the SpaceDeletedObserver type
+type SpaceDeletedObserver struct {
+	mock.Mock
+}
+
+// OnSpaceDeleted provides a mock function with given fields: ctx, space
+func (_m *SpaceDeletedObserver) OnSpaceDeleted(ctx context.Context, space *spaces.Space) error {
+	ret := _m.Called(ctx, space)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Space) error); ok {
+		r0 = rf(ctx, space)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewSpaceDeletedObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewSpaceDeletedObserver creates a new instance of SpaceDeletedObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewSpaceDeletedObserver(t mockConstructorTestingTNewSpaceDeletedObserver) *SpaceDeletedObserver {
+	mock := &SpaceDeletedObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/spaces/mocks/SpaceObserver.go b/pkg/spaces/mocks/SpaceObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..9c5006bf80047884f45637207d3ea6fdf0b702aa
--- /dev/null
+++ b/pkg/spaces/mocks/SpaceObserver.go
@@ -0,0 +1,25 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// SpaceObserver is an autogenerated mock type for the SpaceObserver type
+type SpaceObserver struct {
+	mock.Mock
+}
+
+type mockConstructorTestingTNewSpaceObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewSpaceObserver creates a new instance of SpaceObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewSpaceObserver(t mockConstructorTestingTNewSpaceObserver) *SpaceObserver {
+	mock := &SpaceObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/spaces/mocks/SpaceUpdatedObserver.go b/pkg/spaces/mocks/SpaceUpdatedObserver.go
new file mode 100644
index 0000000000000000000000000000000000000000..a7ced9b2d7bab0618013b5f73cef304c40cecb57
--- /dev/null
+++ b/pkg/spaces/mocks/SpaceUpdatedObserver.go
@@ -0,0 +1,44 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	spaces "git.perx.ru/perxis/perxis-go/pkg/spaces"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// SpaceUpdatedObserver is an autogenerated mock type for the SpaceUpdatedObserver type
+type SpaceUpdatedObserver struct {
+	mock.Mock
+}
+
+// OnSpaceUpdated provides a mock function with given fields: ctx, before, space
+func (_m *SpaceUpdatedObserver) OnSpaceUpdated(ctx context.Context, before *spaces.Space, space *spaces.Space) error {
+	ret := _m.Called(ctx, before, space)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Space, *spaces.Space) error); ok {
+		r0 = rf(ctx, before, space)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+type mockConstructorTestingTNewSpaceUpdatedObserver interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewSpaceUpdatedObserver creates a new instance of SpaceUpdatedObserver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewSpaceUpdatedObserver(t mockConstructorTestingTNewSpaceUpdatedObserver) *SpaceUpdatedObserver {
+	mock := &SpaceUpdatedObserver{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/spaces/mocks/Storage.go b/pkg/spaces/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..5223c772079c3719e1421dafe786498b17e0121c
--- /dev/null
+++ b/pkg/spaces/mocks/Storage.go
@@ -0,0 +1,153 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	mock "github.com/stretchr/testify/mock"
+
+	spaces "git.perx.ru/perxis/perxis-go/pkg/spaces"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, space
+func (_m *Storage) Create(ctx context.Context, space *spaces.Space) error {
+	ret := _m.Called(ctx, space)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Space) error); ok {
+		r0 = rf(ctx, space)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Delete provides a mock function with given fields: ctx, filter
+func (_m *Storage) Delete(ctx context.Context, filter *spaces.Filter) (int, error) {
+	ret := _m.Called(ctx, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Filter) int); ok {
+		r0 = rf(ctx, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *spaces.Filter) error); ok {
+		r1 = rf(ctx, filter)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Find provides a mock function with given fields: ctx, filter, opts
+func (_m *Storage) Find(ctx context.Context, filter *spaces.Filter, opts *options.FindOptions) ([]*spaces.Space, int, error) {
+	ret := _m.Called(ctx, filter, opts)
+
+	var r0 []*spaces.Space
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Filter, *options.FindOptions) []*spaces.Space); ok {
+		r0 = rf(ctx, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*spaces.Space)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *spaces.Filter, *options.FindOptions) int); ok {
+		r1 = rf(ctx, filter, opts)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *spaces.Filter, *options.FindOptions) error); ok {
+		r2 = rf(ctx, filter, opts)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// Init provides a mock function with given fields: ctx
+func (_m *Storage) Init(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Reset provides a mock function with given fields: ctx
+func (_m *Storage) Reset(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, update, filter
+func (_m *Storage) Update(ctx context.Context, update *spaces.Space, filter *spaces.Filter) (int, int, error) {
+	ret := _m.Called(ctx, update, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *spaces.Space, *spaces.Filter) int); ok {
+		r0 = rf(ctx, update, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *spaces.Space, *spaces.Filter) int); ok {
+		r1 = rf(ctx, update, filter)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *spaces.Space, *spaces.Filter) error); ok {
+		r2 = rf(ctx, update, filter)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/spaces/observer.go b/pkg/spaces/observer.go
new file mode 100644
index 0000000000000000000000000000000000000000..b3d3e60b1746fcccfef3cc21439c74d605096e64
--- /dev/null
+++ b/pkg/spaces/observer.go
@@ -0,0 +1,17 @@
+package spaces
+
+import "context"
+
+type SpaceObserver interface{}
+
+type SpaceCreatedObserver interface {
+	OnSpaceCreated(ctx context.Context, space *Space) error
+}
+
+type SpaceUpdatedObserver interface {
+	OnSpaceUpdated(ctx context.Context, before, space *Space) error
+}
+
+type SpaceDeletedObserver interface {
+	OnSpaceDeleted(ctx context.Context, space *Space) error
+}
diff --git a/pkg/spaces/storage.go b/pkg/spaces/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..52baf9da5cfaebf0cfb331698c7284d1eba809a2
--- /dev/null
+++ b/pkg/spaces/storage.go
@@ -0,0 +1,23 @@
+package spaces
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Storage interface {
+	Create(ctx context.Context, space *Space) error
+	Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (spaces []*Space, total int, err error)
+	Delete(ctx context.Context, filter *Filter) (total int, err error)
+	Update(ctx context.Context, update *Space, filter *Filter) (updated, total int, err error)
+	Reset(ctx context.Context) error
+	Init(ctx context.Context) error
+}
+
+type Filter struct {
+	ID    []string `json:"id,omitempty" bson:"_id"`
+	OrgID []string `json:"org_id,omitempty" bson:"orgId"`
+	Name  []string `json:"name,omitempty" bson:"name"`
+	State []State  `json:"state,omitempty" bson:"state"`
+}
diff --git a/pkg/users/middleware/telemetry_middleware.go b/pkg/users/middleware/telemetry_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..a68b365d63d4c7668daf80035ca0f82d3d4bd598
--- /dev/null
+++ b/pkg/users/middleware/telemetry_middleware.go
@@ -0,0 +1,164 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/telemetry
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/users -i Users -t ../../../assets/templates/middleware/telemetry -o telemetry_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// telemetryMiddleware implements users.Users interface instrumented with opentracing spans
+type telemetryMiddleware struct {
+	users.Users
+	_instance      string
+	_spanDecorator func(span trace.Span, params, results map[string]interface{})
+}
+
+// TelemetryMiddleware returns telemetryMiddleware
+func TelemetryMiddleware(base users.Users, instance string, spanDecorator ...func(span trace.Span, params, results map[string]interface{})) telemetryMiddleware {
+	d := telemetryMiddleware{
+		Users:     base,
+		_instance: instance,
+	}
+
+	if len(spanDecorator) > 0 && spanDecorator[0] != nil {
+		d._spanDecorator = spanDecorator[0]
+	}
+
+	return d
+}
+
+// Create implements users.Users
+func (_d telemetryMiddleware) Create(ctx context.Context, create *users.User) (user *users.User, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Users.Create")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"create": create}, map[string]interface{}{
+				"user": user,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Users.Create(ctx, create)
+}
+
+// Delete implements users.Users
+func (_d telemetryMiddleware) Delete(ctx context.Context, userId string) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Users.Delete")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"userId": userId}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Users.Delete(ctx, userId)
+}
+
+// Find implements users.Users
+func (_d telemetryMiddleware) Find(ctx context.Context, filter *users.Filter, options *options.FindOptions) (users []*users.User, total int, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Users.Find")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":     ctx,
+				"filter":  filter,
+				"options": options}, map[string]interface{}{
+				"users": users,
+				"total": total,
+				"err":   err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Users.Find(ctx, filter, options)
+}
+
+// Get implements users.Users
+func (_d telemetryMiddleware) Get(ctx context.Context, userId string) (user *users.User, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Users.Get")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"userId": userId}, map[string]interface{}{
+				"user": user,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Users.Get(ctx, userId)
+}
+
+// GetByIdentity implements users.Users
+func (_d telemetryMiddleware) GetByIdentity(ctx context.Context, identity string) (user *users.User, err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Users.GetByIdentity")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":      ctx,
+				"identity": identity}, map[string]interface{}{
+				"user": user,
+				"err":  err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Users.GetByIdentity(ctx, identity)
+}
+
+// Update implements users.Users
+func (_d telemetryMiddleware) Update(ctx context.Context, update *users.User) (err error) {
+	ctx, _span := otel.Tracer(_d._instance).Start(ctx, "Users.Update")
+	defer func() {
+		if _d._spanDecorator != nil {
+			_d._spanDecorator(_span, map[string]interface{}{
+				"ctx":    ctx,
+				"update": update}, map[string]interface{}{
+				"err": err})
+		} else if err != nil {
+			_span.RecordError(err)
+			_span.SetAttributes(attribute.String("event", "error"))
+			_span.SetAttributes(attribute.String("message", err.Error()))
+		}
+
+		_span.End()
+	}()
+	return _d.Users.Update(ctx, update)
+}
diff --git a/pkg/users/mocks/Storage.go b/pkg/users/mocks/Storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..e918d42414221cc2b6f29abe27116777714df08a
--- /dev/null
+++ b/pkg/users/mocks/Storage.go
@@ -0,0 +1,162 @@
+// Code generated by mockery v2.15.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+
+	options "git.perx.ru/perxis/perxis-go/pkg/options"
+	mock "github.com/stretchr/testify/mock"
+
+	users "git.perx.ru/perxis/perxis-go/pkg/users"
+)
+
+// Storage is an autogenerated mock type for the Storage type
+type Storage struct {
+	mock.Mock
+}
+
+// Create provides a mock function with given fields: ctx, create
+func (_m *Storage) Create(ctx context.Context, create *users.User) (*users.User, error) {
+	ret := _m.Called(ctx, create)
+
+	var r0 *users.User
+	if rf, ok := ret.Get(0).(func(context.Context, *users.User) *users.User); ok {
+		r0 = rf(ctx, create)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*users.User)
+		}
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *users.User) error); ok {
+		r1 = rf(ctx, create)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Delete provides a mock function with given fields: ctx, filter
+func (_m *Storage) Delete(ctx context.Context, filter *users.Filter) (int, error) {
+	ret := _m.Called(ctx, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *users.Filter) int); ok {
+		r0 = rf(ctx, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 error
+	if rf, ok := ret.Get(1).(func(context.Context, *users.Filter) error); ok {
+		r1 = rf(ctx, filter)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// Find provides a mock function with given fields: ctx, filter, opts
+func (_m *Storage) Find(ctx context.Context, filter *users.Filter, opts *options.FindOptions) ([]*users.User, int, error) {
+	ret := _m.Called(ctx, filter, opts)
+
+	var r0 []*users.User
+	if rf, ok := ret.Get(0).(func(context.Context, *users.Filter, *options.FindOptions) []*users.User); ok {
+		r0 = rf(ctx, filter, opts)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*users.User)
+		}
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *users.Filter, *options.FindOptions) int); ok {
+		r1 = rf(ctx, filter, opts)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *users.Filter, *options.FindOptions) error); ok {
+		r2 = rf(ctx, filter, opts)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+// Init provides a mock function with given fields: ctx
+func (_m *Storage) Init(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Reset provides a mock function with given fields: ctx
+func (_m *Storage) Reset(ctx context.Context) error {
+	ret := _m.Called(ctx)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context) error); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Update provides a mock function with given fields: ctx, update, filter
+func (_m *Storage) Update(ctx context.Context, update *users.User, filter *users.Filter) (int, int, error) {
+	ret := _m.Called(ctx, update, filter)
+
+	var r0 int
+	if rf, ok := ret.Get(0).(func(context.Context, *users.User, *users.Filter) int); ok {
+		r0 = rf(ctx, update, filter)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	var r1 int
+	if rf, ok := ret.Get(1).(func(context.Context, *users.User, *users.Filter) int); ok {
+		r1 = rf(ctx, update, filter)
+	} else {
+		r1 = ret.Get(1).(int)
+	}
+
+	var r2 error
+	if rf, ok := ret.Get(2).(func(context.Context, *users.User, *users.Filter) error); ok {
+		r2 = rf(ctx, update, filter)
+	} else {
+		r2 = ret.Error(2)
+	}
+
+	return r0, r1, r2
+}
+
+type mockConstructorTestingTNewStorage interface {
+	mock.TestingT
+	Cleanup(func())
+}
+
+// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+func NewStorage(t mockConstructorTestingTNewStorage) *Storage {
+	mock := &Storage{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/pkg/users/storage.go b/pkg/users/storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..7600525f6cfdb0fb896fa3f477ed6cc493e26eff
--- /dev/null
+++ b/pkg/users/storage.go
@@ -0,0 +1,17 @@
+package users
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+type Storage interface {
+	Create(ctx context.Context, create *User) (user *User, err error)
+	Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (users []*User, total int, err error)
+	Update(ctx context.Context, update *User, filter *Filter) (updated, total int, err error)
+	Delete(ctx context.Context, filter *Filter) (total int, err error)
+
+	Reset(ctx context.Context) error
+	Init(ctx context.Context) error
+}