diff --git a/pkg/collections/collection.go b/pkg/collections/collection.go
index dfe5d66ed4a5b96f4f99118a6842488a2bc94735..ddb73ecad0151b3f185f069ed503f0b4ba4d9165 100644
--- a/pkg/collections/collection.go
+++ b/pkg/collections/collection.go
@@ -176,6 +176,10 @@ func (c Collection) IsNoData() bool {
 	return c.NoData != nil && *c.NoData
 }
 
+func (c Collection) IsSystem() bool {
+	return c.System != nil && *c.System
+}
+
 func (c Collection) IsView() bool {
 	return c.View != nil
 }
diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go
index 26d9b6d51171307161e68ceef0830857fbb569a8..fb8044c7a2f4c76b775840c5628319196a0493ae 100644
--- a/pkg/extension/extension.go
+++ b/pkg/extension/extension.go
@@ -21,6 +21,8 @@ const (
 	StateInstalled  = pb.SpaceExtensions_INSTALLED
 	StateInProgress = pb.SpaceExtensions_IN_PROGRESS
 	StateFail       = pb.SpaceExtensions_FAIL
+
+	ExtensionMetadataKey = "extension"
 )
 
 type (
diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go
index e0eca6b6ffc02511acbe3712b2f2bf83fc5bc05e..bac95d05de2e863e5540c02f8553e431b2f602c7 100644
--- a/pkg/schema/schema.go
+++ b/pkg/schema/schema.go
@@ -12,7 +12,8 @@ import (
 
 type Schema struct {
 	field.Field
-	Loaded bool `json:"loaded"`
+	Loaded   bool              `json:"loaded"`
+	Metadata map[string]string `json:"metadata"`
 }
 
 func New(kv ...interface{}) *Schema {
@@ -33,8 +34,9 @@ var (
 
 func (s *Schema) Clone(reset bool) *Schema {
 	return &Schema{
-		Field:  *s.Field.Clone(reset),
-		Loaded: s.Loaded,
+		Field:    *s.Field.Clone(reset),
+		Loaded:   s.Loaded,
+		Metadata: s.Metadata,
 	}
 }
 
@@ -43,6 +45,21 @@ func (s Schema) WithIncludes(includes ...interface{}) *Schema {
 	return &s
 }
 
+func (s *Schema) WithMetadata(kv ...string) *Schema {
+	if s.Metadata == nil {
+		s.Metadata = make(map[string]string, len(s.Metadata))
+	}
+	for i := 0; i < len(kv); i += 2 {
+		s.Metadata[kv[i]] = kv[i+1]
+	}
+	return s
+}
+
+func (s Schema) SetMetadata(md map[string]string) *Schema {
+	s.Metadata = md
+	return &s
+}
+
 func (s *Schema) Load(ctx context.Context) error {
 	if s.Loaded {
 		return nil
diff --git a/pkg/schema/schema_json.go b/pkg/schema/schema_json.go
index 906acb5d0d361611d01f43d10bd0f80377ccb4a7..e8710f76dfb8a5a81da279e4ba6d46f77a1bbdb8 100644
--- a/pkg/schema/schema_json.go
+++ b/pkg/schema/schema_json.go
@@ -6,8 +6,8 @@ import (
 )
 
 type jsonSchema struct {
-	//Field  json.RawMessage `json:"field,inline"`
-	Loaded bool `json:"loaded"`
+	Loaded   bool              `json:"loaded"`
+	Metadata map[string]string `json:"metadata"`
 }
 
 func (s *Schema) UnmarshalJSON(b []byte) error {
@@ -17,6 +17,7 @@ func (s *Schema) UnmarshalJSON(b []byte) error {
 		return errors.Wrapf(err, "error unmarshal json into field")
 	}
 	s.Loaded = j.Loaded
+	s.Metadata = j.Metadata
 
 	if err := s.Field.UnmarshalJSON(b); err != nil {
 		return err
@@ -42,8 +43,8 @@ func (s *Schema) MarshalJSON() ([]byte, error) {
 	}
 
 	jsonSch, err := jsoniter.Marshal(jsonSchema{
-		//Field: b,
-		Loaded: s.Loaded,
+		Loaded:   s.Loaded,
+		Metadata: s.Metadata,
 	})
 	if err != nil {
 		return nil, err
diff --git a/pkg/setup/collection.go b/pkg/setup/collection.go
index 11274063f34d35e095380d27c94387f242339160..f8d119db2b54bc8a9c44c389aafc3e041aafcd1d 100644
--- a/pkg/setup/collection.go
+++ b/pkg/setup/collection.go
@@ -6,8 +6,10 @@ import (
 	"strings"
 
 	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/data"
 	"git.perx.ru/perxis/perxis-go/pkg/environments"
 	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/extension"
 	"go.uber.org/zap"
 )
 
@@ -18,8 +20,8 @@ var (
 )
 
 type CollectionsOption func(c *CollectionConfig)
-type UpdateCollectionFn func(s *Setup, exist, new *collections.Collection) (coll *collections.Collection, upd bool, setSchema bool)
-type DeleteCollectionFn func(s *Setup, col *collections.Collection) bool
+type UpdateCollectionFn func(s *Setup, exist, new *collections.Collection) (coll *collections.Collection, upd bool, setSchema bool, err error)
+type DeleteCollectionFn func(s *Setup, col *collections.Collection) (bool, error)
 
 type CollectionConfig struct {
 	collection *collections.Collection
@@ -30,7 +32,7 @@ type CollectionConfig struct {
 func NewCollectionConfig(collection *collections.Collection, opt ...CollectionsOption) CollectionConfig {
 	c := CollectionConfig{collection: collection}
 
-	UpdateExistingCollection()(&c)
+	DefaultUpdateCollectionStrategy()(&c)
 	DeleteCollectionIfRemove()(&c)
 
 	for _, o := range opt {
@@ -42,40 +44,55 @@ func NewCollectionConfig(collection *collections.Collection, opt ...CollectionsO
 
 func OverwriteCollection() CollectionsOption {
 	return func(c *CollectionConfig) {
-		c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool) {
-			return new, true, true
+		c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool, error) {
+			return new, true, true, nil
 		}
 	}
 }
 
 func KeepExistingCollection() CollectionsOption {
 	return func(c *CollectionConfig) {
-		c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool) {
-			return old, false, false
+		c.UpdateFn = func(s *Setup, old, new *collections.Collection) (*collections.Collection, bool, bool, error) {
+			return old, false, false, nil
 		}
 	}
 }
 
 func DeleteCollection() CollectionsOption {
 	return func(c *CollectionConfig) {
-		c.DeleteFn = func(s *Setup, collection *collections.Collection) bool { return true }
+		c.DeleteFn = func(s *Setup, collection *collections.Collection) (bool, error) { return true, nil }
 	}
 }
 
 func DeleteCollectionIfRemove() CollectionsOption {
 	return func(c *CollectionConfig) {
-		c.DeleteFn = func(s *Setup, collection *collections.Collection) bool { return s.IsRemove() }
+		c.DeleteFn = func(s *Setup, collection *collections.Collection) (bool, error) { return s.IsRemove(), nil }
 	}
 }
 
-func UpdateExistingCollection() CollectionsOption {
+func DefaultUpdateCollectionStrategy() CollectionsOption {
 	return func(c *CollectionConfig) {
-		c.UpdateFn = func(s *Setup, exist, collection *collections.Collection) (*collections.Collection, bool, bool) {
+		c.UpdateFn = func(s *Setup, exist, collection *collections.Collection) (*collections.Collection, bool, bool, error) {
+			if !s.IsForce() && !collection.IsView() && !exist.IsView() && collection.Schema.Metadata != nil && collection.Schema.Metadata[extension.ExtensionMetadataKey] != "" {
+				if exist.Schema.Metadata == nil || exist.Schema.Metadata[extension.ExtensionMetadataKey] != collection.Schema.Metadata[extension.ExtensionMetadataKey] {
+					return nil, false, false, collections.ErrAlreadyExists
+				}
+			}
+
 			if len(exist.Tags) > 0 {
-				collection.Tags = append(exist.Tags, collection.Tags...)
+				collection.Tags = data.SetFromSlice(append(exist.Tags, collection.Tags...))
+			}
+
+			var update, setSchema bool
+			update = collection.Name != exist.Name || collection.IsSingle() != exist.IsSingle() || collection.IsSystem() != exist.IsSystem() ||
+				collection.IsNoData() != exist.IsNoData() || collection.Hidden != exist.Hidden || collection.IsView() != exist.IsView() && data.ElementsMatch(exist.Tags, collection.Tags)
+
+			if exist.View != nil && collection.View != nil {
+				update = update && *exist.View == *collection.View
 			}
+			setSchema = !collection.IsView() && !reflect.DeepEqual(exist.Schema, collection.Schema)
 
-			return collection, true, !collection.IsView() && !reflect.DeepEqual(exist.Schema, collection.Schema)
+			return collection, update, setSchema, nil
 		}
 	}
 }
@@ -133,13 +150,16 @@ func (s *Setup) InstallCollection(ctx context.Context, c CollectionConfig) (setS
 
 	if exist == nil {
 		setSchema = !collection.IsView()
-		exist, err = s.content.Collections.Create(ctx, collection)
+		_, err = s.content.Collections.Create(ctx, collection)
 		if err != nil {
 			return false, err
 		}
 	} else {
 		var upd bool
-		collection, upd, setSchema = c.UpdateFn(s, exist, c.collection)
+		collection, upd, setSchema, err = c.UpdateFn(s, exist, c.collection)
+		if err != nil {
+			return false, err
+		}
 		if upd {
 			if err = s.content.Collections.Update(ctx, collection); err != nil {
 				return false, err
@@ -205,8 +225,12 @@ func (s *Setup) UninstallCollections(ctx context.Context) error {
 }
 
 func (s *Setup) UninstallCollection(ctx context.Context, c CollectionConfig) error {
-	if c.DeleteFn(s, c.collection) {
-		if err := s.content.Collections.Delete(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID); err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) {
+	ok, err := c.DeleteFn(s, c.collection)
+	if err != nil {
+		return err
+	}
+	if ok {
+		if err = s.content.Collections.Delete(ctx, s.SpaceID, s.EnvironmentID, c.collection.ID); err != nil && !strings.Contains(err.Error(), collections.ErrNotFound.Error()) {
 			return err
 		}
 		s.removeItems(c.collection.ID) // после удаления коллекции нет смысла удалять ее элементы
diff --git a/pkg/setup/collection_test.go b/pkg/setup/collection_test.go
index 313b04bb3e272b3ee0181a38da41f0cd1106ca30..b249048970b153d08f30f8aecf6f732c827cf47e 100644
--- a/pkg/setup/collection_test.go
+++ b/pkg/setup/collection_test.go
@@ -10,6 +10,7 @@ import (
 	"git.perx.ru/perxis/perxis-go/pkg/environments"
 	envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
 	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/extension"
 	"git.perx.ru/perxis/perxis-go/pkg/schema"
 	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
 	"github.com/stretchr/testify/assert"
@@ -22,6 +23,7 @@ func TestSetup_InstallCollections(t *testing.T) {
 		collections     []*collections.Collection
 		collectionsCall func(svc *mockscollections.Collections)
 		envsCall        func(svc *envmocks.Environments)
+		force           bool
 		wantErr         func(t *testing.T, err error)
 	}{
 		{
@@ -91,6 +93,77 @@ func TestSetup_InstallCollections(t *testing.T) {
 				assert.Contains(t, errors.GetDetail(err), "Возникла ошибка при настройке коллекции space(1)")
 			},
 		},
+		{
+			name:        "Update extension collection with metadata",
+			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}},
+			collectionsCall: func(svc *mockscollections.Collections) {
+				svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}, nil).Once()
+				svc.On("Update", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}).Return(nil).Once()
+				svc.On("SetSchema", mock.Anything, "sp", "env", "1", schema.New("name", field.String())).Return(nil).Once()
+			},
+			envsCall: func(svc *envmocks.Environments) {
+				svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:        "Fail to update user collection with same id as in extensions collection",
+			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}},
+			collectionsCall: func(svc *mockscollections.Collections) {
+				svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String())}, nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.Error(t, err)
+				assert.ErrorIs(t, err, collections.ErrAlreadyExists)
+			},
+		},
+		{
+			name:        "Update user collection with same id as in extensions collection with force",
+			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}},
+			collectionsCall: func(svc *mockscollections.Collections) {
+				svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String())}, nil).Once()
+				svc.On("Update", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}).Return(nil).Once()
+				svc.On("SetSchema", mock.Anything, "sp", "env", "1", schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")).Return(nil).Once()
+			},
+			envsCall: func(svc *envmocks.Environments) {
+				svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+			force: true,
+		},
+		{
+			name:        "Update exist view collection with the same id",
+			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}},
+			collectionsCall: func(svc *mockscollections.Collections) {
+				svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp2", EnvID: environments.DefaultEnvironment, CollectionID: "2"}}, nil).Once()
+				svc.On("Update", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", Schema: schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")}).Return(nil).Once()
+				svc.On("SetSchema", mock.Anything, "sp", "env", "1", schema.New("name", field.String()).WithMetadata(extension.ExtensionMetadataKey, "test-extension")).Return(nil).Once()
+			},
+			envsCall: func(svc *envmocks.Environments) {
+				svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
+		{
+			name:        "Update view collection with the same id to new view collection",
+			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp3", EnvID: environments.DefaultEnvironment, CollectionID: "3"}}},
+			collectionsCall: func(svc *mockscollections.Collections) {
+				svc.On("Get", mock.Anything, "sp", "env", "1").Return(&collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp2", EnvID: environments.DefaultEnvironment, CollectionID: "2"}}, nil).Once()
+				svc.On("Update", mock.Anything, &collections.Collection{ID: "1", SpaceID: "sp", EnvID: "env", View: &collections.View{SpaceID: "sp3", EnvID: environments.DefaultEnvironment, CollectionID: "3"}}).Return(nil).Once()
+			},
+			envsCall: func(svc *envmocks.Environments) {
+				svc.On("Migrate", mock.Anything, "sp", "env", &environments.MigrateOptions{Wait: true}).Return(nil).Once()
+			},
+			wantErr: func(t *testing.T, err error) {
+				assert.NoError(t, err)
+			},
+		},
 		{
 			name:        "Fail to install collection on migrate",
 			collections: []*collections.Collection{{ID: "1", SpaceID: "sp", Name: "space", EnvID: "env", Schema: schema.New("name", field.String())}},
@@ -121,7 +194,7 @@ func TestSetup_InstallCollections(t *testing.T) {
 				tt.envsCall(e)
 			}
 
-			s := NewSetup(&content.Content{Collections: c, Environments: e}, "sp", "env", nil)
+			s := NewSetup(&content.Content{Collections: c, Environments: e}, "sp", "env", nil).WithForce(tt.force)
 			s.AddCollections(tt.collections)
 			tt.wantErr(t, s.InstallCollections(context.Background()))
 		})
diff --git a/pkg/setup/setup_test.go b/pkg/setup/setup_test.go
index 72773e29fa9faeb7c474d270081aee54e3dd03b7..7c2059732443bd697f4e9b618f36569e45626fa9 100644
--- a/pkg/setup/setup_test.go
+++ b/pkg/setup/setup_test.go
@@ -181,15 +181,9 @@ func TestSetupInstall(t *testing.T) {
 		envMocks := &environmentMock.Environments{}
 
 		collsMock := &collectionMock.Collections{}
-		for _, collection := range getCollections() {
-			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
-				Return(collection, nil).
-				Once()
-
-			collsMock.On("Update", mock.Anything, collection).
-				Return(nil).
-				Once()
-		}
+		collsMock.On("Get", mock.Anything, spaceID, envID, "coll1").
+			Return(&collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция старая"}, nil).Once()
+		collsMock.On("Update", mock.Anything, &collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция"}).Return(nil).Once()
 
 		rMock := &rolesMock.Roles{}
 		for _, role := range getRoles() {
@@ -247,15 +241,9 @@ func TestSetupInstall(t *testing.T) {
 
 	t.Run("Success, with force", func(t *testing.T) {
 		collsMock := &collectionMock.Collections{}
-		for _, collection := range getCollections() {
-			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
-				Return(collection, nil).
-				Once()
-
-			collsMock.On("Update", mock.Anything, collection).
-				Return(nil).
-				Once()
-		}
+		collsMock.On("Get", mock.Anything, spaceID, envID, "coll1").
+			Return(&collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция старая"}, nil).Once()
+		collsMock.On("Update", mock.Anything, &collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция"}).Return(nil).Once()
 
 		rMock := &rolesMock.Roles{}
 		for _, role := range getRoles() {
@@ -453,15 +441,8 @@ func TestSetupInstall(t *testing.T) {
 
 	t.Run("Can't update collection, storage returns error", func(t *testing.T) {
 		collsMock := &collectionMock.Collections{}
-		for _, collection := range getCollections() {
-			collsMock.On("Get", mock.Anything, spaceID, envID, collection.ID).
-				Return(collection, nil).
-				Once()
-
-			collsMock.On("Update", mock.Anything, collection).
-				Return(errors.New("can't update collection")).
-				Once()
-		}
+		collsMock.On("Get", mock.Anything, spaceID, envID, "coll1").Return(&collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция 00"}, nil).Once()
+		collsMock.On("Update", mock.Anything, &collections.Collection{ID: "coll1", SpaceID: spaceID, EnvID: envID, Schema: schema.New(), Name: "Коллекция"}).Return(errors.New("can't update collection")).Once()
 
 		rMock := &rolesMock.Roles{}
 		for _, role := range getRoles() {