Repository: textileio/go-threads
Pull request #436 (2020-09-15 00:36:22)

DB: Adds a verify save method, filter read before creating save actions
- When generating patches, the current value must be filtered for the given identity otherwise weird deletions can happen (and they will get permission denied).
- Adds a `Verify` method that function much like `Save` but does _not_ commit the generated actions. This is useful in a transaction to know if a future action will be permitted.


Patch:
diff --git a/api/client/client.go b/api/client/client.go
index 6af587d..bdb0510 100644
--- a/api/client/client.go
+++ b/api/client/client.go
@@ -447,6 +447,25 @@ func (c *Client) Create(ctx context.Context, dbID thread.ID, collectionName stri
 	return resp.GetInstanceIDs(), nil
 }
 
+// Verify verifies existing instance changes.
+func (c *Client) Verify(ctx context.Context, dbID thread.ID, collectionName string, instances Instances, opts ...db.TxnOption) error {
+	args := &db.TxnOptions{}
+	for _, opt := range opts {
+		opt(args)
+	}
+	values, err := marshalItems(instances)
+	if err != nil {
+		return err
+	}
+	ctx = thread.NewTokenContext(ctx, args.Token)
+	_, err = c.c.Verify(ctx, &pb.VerifyRequest{
+		DbID:           dbID.Bytes(),
+		CollectionName: collectionName,
+		Instances:      values,
+	})
+	return err
+}
+
 // Save saves existing instances.
 func (c *Client) Save(ctx context.Context, dbID thread.ID, collectionName string, instances Instances, opts ...db.TxnOption) error {
 	args := &db.TxnOptions{}
diff --git a/api/client/read.go b/api/client/read.go
index fd16494..4d50c1c 100644
--- a/api/client/read.go
+++ b/api/client/read.go
@@ -3,6 +3,7 @@ package client
 import (
 	"encoding/json"
 	"fmt"
+	"io"
 
 	pb "github.com/textileio/go-threads/api/pb"
 	"github.com/textileio/go-threads/core/thread"
@@ -120,5 +121,11 @@ func (t *ReadTransaction) Find(query *db.Query, dummy interface{}) (interface{},
 
 // end ends the active transaction.
 func (t *ReadTransaction) end() error {
-	return t.client.CloseSend()
+	if err := t.client.CloseSend(); err != nil {
+		return err
+	}
+	if _, err := t.client.Recv(); err != nil && err != io.EOF {
+		return err
+	}
+	return nil
 }
diff --git a/api/client/write.go b/api/client/write.go
index aa30776..ca0302c 100644
--- a/api/client/write.go
+++ b/api/client/write.go
@@ -3,6 +3,7 @@ package client
 import (
 	"encoding/json"
 	"fmt"
+	"io"
 
 	pb "github.com/textileio/go-threads/api/pb"
 	"github.com/textileio/go-threads/core/thread"
@@ -144,6 +145,35 @@ func (t *WriteTransaction) Create(items ...interface{}) ([]string, error) {
 	}
 }
 
+// Verify verifies existing instance changes.
+func (t *WriteTransaction) Verify(items ...interface{}) error {
+	values, err := marshalItems(items)
+	if err != nil {
+		return err
+	}
+	innerReq := &pb.VerifyRequest{
+		Instances: values,
+	}
+	option := &pb.WriteTransactionRequest_VerifyRequest{
+		VerifyRequest: innerReq,
+	}
+	if err = t.client.Send(&pb.WriteTransactionRequest{
+		Option: option,
+	}); err != nil {
+		return err
+	}
+	var resp *pb.WriteTransactionReply
+	if resp, err = t.client.Recv(); err != nil {
+		return err
+	}
+	switch x := resp.GetOption().(type) {
+	case *pb.WriteTransactionReply_VerifyReply:
+		return nil
+	default:
+		return fmt.Errorf("WriteTransactionReply.Option has unexpected type %T", x)
+	}
+}
+
 // Save saves existing instances.
 func (t *WriteTransaction) Save(items ...interface{}) error {
 	values, err := marshalItems(items)
@@ -201,5 +231,11 @@ func (t *WriteTransaction) Delete(instanceIDs ...string) error {
 
 // end ends the active transaction.
 func (t *WriteTransaction) end() error {
-	return t.client.CloseSend()
+	if err := t.client.CloseSend(); err != nil {
+		return err
+	}
+	if _, err := t.client.Recv(); err != nil && err != io.EOF {
+		return err
+	}
+	return nil
 }
diff --git a/api/pb/threads.pb.go b/api/pb/threads.pb.go
index 5c0984d..167a7be 100644
--- a/api/pb/threads.pb.go
+++ b/api/pb/threads.pb.go
@@ -52,7 +52,7 @@ func (x ListenRequest_Filter_Action) String() string {
 }
 
 func (ListenRequest_Filter_Action) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{42, 0, 0}
+	return fileDescriptor_f2ba358bb2150022, []int{44, 0, 0}
 }
 
 type ListenReply_Action int32
@@ -80,7 +80,7 @@ func (x ListenReply_Action) String() string {
 }
 
 func (ListenReply_Action) EnumDescriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{43, 0}
+	return fileDescriptor_f2ba358bb2150022, []int{45, 0}
 }
 
 type GetTokenRequest struct {
@@ -1409,6 +1409,92 @@ func (m *CreateReply) GetInstanceIDs() []string {
 	return nil
 }
 
+type VerifyRequest struct {
+	DbID                 []byte   `protobuf:"bytes,1,opt,name=dbID,proto3" json:"dbID,omitempty"`
+	CollectionName       string   `protobuf:"bytes,2,opt,name=collectionName,proto3" json:"collectionName,omitempty"`
+	Instances            [][]byte `protobuf:"bytes,3,rep,name=instances,proto3" json:"instances,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *VerifyRequest) Reset()         { *m = VerifyRequest{} }
+func (m *VerifyRequest) String() string { return proto.CompactTextString(m) }
+func (*VerifyRequest) ProtoMessage()    {}
+func (*VerifyRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f2ba358bb2150022, []int{27}
+}
+
+func (m *VerifyRequest) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_VerifyRequest.Unmarshal(m, b)
+}
+func (m *VerifyRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_VerifyRequest.Marshal(b, m, deterministic)
+}
+func (m *VerifyRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_VerifyRequest.Merge(m, src)
+}
+func (m *VerifyRequest) XXX_Size() int {
+	return xxx_messageInfo_VerifyRequest.Size(m)
+}
+func (m *VerifyRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_VerifyRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_VerifyRequest proto.InternalMessageInfo
+
+func (m *VerifyRequest) GetDbID() []byte {
+	if m != nil {
+		return m.DbID
+	}
+	return nil
+}
+
+func (m *VerifyRequest) GetCollectionName() string {
+	if m != nil {
+		return m.CollectionName
+	}
+	return ""
+}
+
+func (m *VerifyRequest) GetInstances() [][]byte {
+	if m != nil {
+		return m.Instances
+	}
+	return nil
+}
+
+type VerifyReply struct {
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *VerifyReply) Reset()         { *m = VerifyReply{} }
+func (m *VerifyReply) String() string { return proto.CompactTextString(m) }
+func (*VerifyReply) ProtoMessage()    {}
+func (*VerifyReply) Descriptor() ([]byte, []int) {
+	return fileDescriptor_f2ba358bb2150022, []int{28}
+}
+
+func (m *VerifyReply) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_VerifyReply.Unmarshal(m, b)
+}
+func (m *VerifyReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_VerifyReply.Marshal(b, m, deterministic)
+}
+func (m *VerifyReply) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_VerifyReply.Merge(m, src)
+}
+func (m *VerifyReply) XXX_Size() int {
+	return xxx_messageInfo_VerifyReply.Size(m)
+}
+func (m *VerifyReply) XXX_DiscardUnknown() {
+	xxx_messageInfo_VerifyReply.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_VerifyReply proto.InternalMessageInfo
+
 type SaveRequest struct {
 	DbID                 []byte   `protobuf:"bytes,1,opt,name=dbID,proto3" json:"dbID,omitempty"`
 	CollectionName       string   `protobuf:"bytes,2,opt,name=collectionName,proto3" json:"collectionName,omitempty"`
@@ -1422,7 +1508,7 @@ func (m *SaveRequest) Reset()         { *m = SaveRequest{} }
 func (m *SaveRequest) String() string { return proto.CompactTextString(m) }
 func (*SaveRequest) ProtoMessage()    {}
 func (*SaveRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{27}
+	return fileDescriptor_f2ba358bb2150022, []int{29}
 }
 
 func (m *SaveRequest) XXX_Unmarshal(b []byte) error {
@@ -1474,7 +1560,7 @@ func (m *SaveReply) Reset()         { *m = SaveReply{} }
 func (m *SaveReply) String() string { return proto.CompactTextString(m) }
 func (*SaveReply) ProtoMessage()    {}
 func (*SaveReply) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{28}
+	return fileDescriptor_f2ba358bb2150022, []int{30}
 }
 
 func (m *SaveReply) XXX_Unmarshal(b []byte) error {
@@ -1508,7 +1594,7 @@ func (m *DeleteRequest) Reset()         { *m = DeleteRequest{} }
 func (m *DeleteRequest) String() string { return proto.CompactTextString(m) }
 func (*DeleteRequest) ProtoMessage()    {}
 func (*DeleteRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{29}
+	return fileDescriptor_f2ba358bb2150022, []int{31}
 }
 
 func (m *DeleteRequest) XXX_Unmarshal(b []byte) error {
@@ -1560,7 +1646,7 @@ func (m *DeleteReply) Reset()         { *m = DeleteReply{} }
 func (m *DeleteReply) String() string { return proto.CompactTextString(m) }
 func (*DeleteReply) ProtoMessage()    {}
 func (*DeleteReply) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{30}
+	return fileDescriptor_f2ba358bb2150022, []int{32}
 }
 
 func (m *DeleteReply) XXX_Unmarshal(b []byte) error {
@@ -1594,7 +1680,7 @@ func (m *HasRequest) Reset()         { *m = HasRequest{} }
 func (m *HasRequest) String() string { return proto.CompactTextString(m) }
 func (*HasRequest) ProtoMessage()    {}
 func (*HasRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{31}
+	return fileDescriptor_f2ba358bb2150022, []int{33}
 }
 
 func (m *HasRequest) XXX_Unmarshal(b []byte) error {
@@ -1647,7 +1733,7 @@ func (m *HasReply) Reset()         { *m = HasReply{} }
 func (m *HasReply) String() string { return proto.CompactTextString(m) }
 func (*HasReply) ProtoMessage()    {}
 func (*HasReply) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{32}
+	return fileDescriptor_f2ba358bb2150022, []int{34}
 }
 
 func (m *HasReply) XXX_Unmarshal(b []byte) error {
@@ -1688,7 +1774,7 @@ func (m *FindRequest) Reset()         { *m = FindRequest{} }
 func (m *FindRequest) String() string { return proto.CompactTextString(m) }
 func (*FindRequest) ProtoMessage()    {}
 func (*FindRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{33}
+	return fileDescriptor_f2ba358bb2150022, []int{35}
 }
 
 func (m *FindRequest) XXX_Unmarshal(b []byte) error {
@@ -1741,7 +1827,7 @@ func (m *FindReply) Reset()         { *m = FindReply{} }
 func (m *FindReply) String() string { return proto.CompactTextString(m) }
 func (*FindReply) ProtoMessage()    {}
 func (*FindReply) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{34}
+	return fileDescriptor_f2ba358bb2150022, []int{36}
 }
 
 func (m *FindReply) XXX_Unmarshal(b []byte) error {
@@ -1782,7 +1868,7 @@ func (m *FindByIDRequest) Reset()         { *m = FindByIDRequest{} }
 func (m *FindByIDRequest) String() string { return proto.CompactTextString(m) }
 func (*FindByIDRequest) ProtoMessage()    {}
 func (*FindByIDRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{35}
+	return fileDescriptor_f2ba358bb2150022, []int{37}
 }
 
 func (m *FindByIDRequest) XXX_Unmarshal(b []byte) error {
@@ -1835,7 +1921,7 @@ func (m *FindByIDReply) Reset()         { *m = FindByIDReply{} }
 func (m *FindByIDReply) String() string { return proto.CompactTextString(m) }
 func (*FindByIDReply) ProtoMessage()    {}
 func (*FindByIDReply) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{36}
+	return fileDescriptor_f2ba358bb2150022, []int{38}
 }
 
 func (m *FindByIDReply) XXX_Unmarshal(b []byte) error {
@@ -1875,7 +1961,7 @@ func (m *StartTransactionRequest) Reset()         { *m = StartTransactionRequest
 func (m *StartTransactionRequest) String() string { return proto.CompactTextString(m) }
 func (*StartTransactionRequest) ProtoMessage()    {}
 func (*StartTransactionRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{37}
+	return fileDescriptor_f2ba358bb2150022, []int{39}
 }
 
 func (m *StartTransactionRequest) XXX_Unmarshal(b []byte) error {
@@ -1926,7 +2012,7 @@ func (m *ReadTransactionRequest) Reset()         { *m = ReadTransactionRequest{}
 func (m *ReadTransactionRequest) String() string { return proto.CompactTextString(m) }
 func (*ReadTransactionRequest) ProtoMessage()    {}
 func (*ReadTransactionRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{38}
+	return fileDescriptor_f2ba358bb2150022, []int{40}
 }
 
 func (m *ReadTransactionRequest) XXX_Unmarshal(b []byte) error {
@@ -2035,7 +2121,7 @@ func (m *ReadTransactionReply) Reset()         { *m = ReadTransactionReply{} }
 func (m *ReadTransactionReply) String() string { return proto.CompactTextString(m) }
 func (*ReadTransactionReply) ProtoMessage()    {}
 func (*ReadTransactionReply) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{39}
+	return fileDescriptor_f2ba358bb2150022, []int{41}
 }
 
 func (m *ReadTransactionReply) XXX_Unmarshal(b []byte) error {
@@ -2119,6 +2205,7 @@ type WriteTransactionRequest struct {
 	// Types that are valid to be assigned to Option:
 	//	*WriteTransactionRequest_StartTransactionRequest
 	//	*WriteTransactionRequest_CreateRequest
+	//	*WriteTransactionRequest_VerifyRequest
 	//	*WriteTransactionRequest_SaveRequest
 	//	*WriteTransactionRequest_DeleteRequest
 	//	*WriteTransactionRequest_HasRequest
@@ -2134,7 +2221,7 @@ func (m *WriteTransactionRequest) Reset()         { *m = WriteTransactionRequest
 func (m *WriteTransactionRequest) String() string { return proto.CompactTextString(m) }
 func (*WriteTransactionRequest) ProtoMessage()    {}
 func (*WriteTransactionRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{40}
+	return fileDescriptor_f2ba358bb2150022, []int{42}
 }
 
 func (m *WriteTransactionRequest) XXX_Unmarshal(b []byte) error {
@@ -2167,6 +2254,10 @@ type WriteTransactionRequest_CreateRequest struct {
 	CreateRequest *CreateRequest `protobuf:"bytes,2,opt,name=createRequest,proto3,oneof"`
 }
 
+type WriteTransactionRequest_VerifyRequest struct {
+	VerifyRequest *VerifyRequest `protobuf:"bytes,8,opt,name=verifyRequest,proto3,oneof"`
+}
+
 type WriteTransactionRequest_SaveRequest struct {
 	SaveRequest *SaveRequest `protobuf:"bytes,3,opt,name=saveRequest,proto3,oneof"`
 }
@@ -2191,6 +2282,8 @@ func (*WriteTransactionRequest_StartTransactionRequest) isWriteTransactionReques
 
 func (*WriteTransactionRequest_CreateRequest) isWriteTransactionRequest_Option() {}
 
+func (*WriteTransactionRequest_VerifyRequest) isWriteTransactionRequest_Option() {}
+
 func (*WriteTransactionRequest_SaveRequest) isWriteTransactionRequest_Option() {}
 
 func (*WriteTransactionRequest_DeleteRequest) isWriteTransactionRequest_Option() {}
@@ -2222,6 +2315,13 @@ func (m *WriteTransactionRequest) GetCreateRequest() *CreateRequest {
 	return nil
 }
 
+func (m *WriteTransactionRequest) GetVerifyRequest() *VerifyRequest {
+	if x, ok := m.GetOption().(*WriteTransactionRequest_VerifyRequest); ok {
+		return x.VerifyRequest
+	}
+	return nil
+}
+
 func (m *WriteTransactionRequest) GetSaveRequest() *SaveRequest {
 	if x, ok := m.GetOption().(*WriteTransactionRequest_SaveRequest); ok {
 		return x.SaveRequest
@@ -2262,6 +2362,7 @@ func (*WriteTransactionRequest) XXX_OneofWrappers() []interface{} {
 	return []interface{}{
 		(*WriteTransactionRequest_StartTransactionRequest)(nil),
 		(*WriteTransactionRequest_CreateRequest)(nil),
+		(*WriteTransactionRequest_VerifyRequest)(nil),
 		(*WriteTransactionRequest_SaveRequest)(nil),
 		(*WriteTransactionRequest_DeleteRequest)(nil),
 		(*WriteTransactionRequest_HasRequest)(nil),
@@ -2273,6 +2374,7 @@ func (*WriteTransactionRequest) XXX_OneofWrappers() []interface{} {
 type WriteTransactionReply struct {
 	// Types that are valid to be assigned to Option:
 	//	*WriteTransactionReply_CreateReply
+	//	*WriteTransactionReply_VerifyReply
 	//	*WriteTransactionReply_SaveReply
 	//	*WriteTransactionReply_DeleteReply
 	//	*WriteTransactionReply_HasReply
@@ -2288,7 +2390,7 @@ func (m *WriteTransactionReply) Reset()         { *m = WriteTransactionReply{} }
 func (m *WriteTransactionReply) String() string { return proto.CompactTextString(m) }
 func (*WriteTransactionReply) ProtoMessage()    {}
 func (*WriteTransactionReply) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{41}
+	return fileDescriptor_f2ba358bb2150022, []int{43}
 }
 
 func (m *WriteTransactionReply) XXX_Unmarshal(b []byte) error {
@@ -2317,6 +2419,10 @@ type WriteTransactionReply_CreateReply struct {
 	CreateReply *CreateReply `protobuf:"bytes,1,opt,name=createReply,proto3,oneof"`
 }
 
+type WriteTransactionReply_VerifyReply struct {
+	VerifyReply *VerifyReply `protobuf:"bytes,7,opt,name=verifyReply,proto3,oneof"`
+}
+
 type WriteTransactionReply_SaveReply struct {
 	SaveReply *SaveReply `protobuf:"bytes,2,opt,name=saveReply,proto3,oneof"`
 }
@@ -2339,6 +2445,8 @@ type WriteTransactionReply_FindByIDReply struct {
 
 func (*WriteTransactionReply_CreateReply) isWriteTransactionReply_Option() {}
 
+func (*WriteTransactionReply_VerifyReply) isWriteTransactionReply_Option() {}
+
 func (*WriteTransactionReply_SaveReply) isWriteTransactionReply_Option() {}
 
 func (*WriteTransactionReply_DeleteReply) isWriteTransactionReply_Option() {}
@@ -2363,6 +2471,13 @@ func (m *WriteTransactionReply) GetCreateReply() *CreateReply {
 	return nil
 }
 
+func (m *WriteTransactionReply) GetVerifyReply() *VerifyReply {
+	if x, ok := m.GetOption().(*WriteTransactionReply_VerifyReply); ok {
+		return x.VerifyReply
+	}
+	return nil
+}
+
 func (m *WriteTransactionReply) GetSaveReply() *SaveReply {
 	if x, ok := m.GetOption().(*WriteTransactionReply_SaveReply); ok {
 		return x.SaveReply
@@ -2402,6 +2517,7 @@ func (m *WriteTransactionReply) GetFindByIDReply() *FindByIDReply {
 func (*WriteTransactionReply) XXX_OneofWrappers() []interface{} {
 	return []interface{}{
 		(*WriteTransactionReply_CreateReply)(nil),
+		(*WriteTransactionReply_VerifyReply)(nil),
 		(*WriteTransactionReply_SaveReply)(nil),
 		(*WriteTransactionReply_DeleteReply)(nil),
 		(*WriteTransactionReply_HasReply)(nil),
@@ -2422,7 +2538,7 @@ func (m *ListenRequest) Reset()         { *m = ListenRequest{} }
 func (m *ListenRequest) String() string { return proto.CompactTextString(m) }
 func (*ListenRequest) ProtoMessage()    {}
 func (*ListenRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{42}
+	return fileDescriptor_f2ba358bb2150022, []int{44}
 }
 
 func (m *ListenRequest) XXX_Unmarshal(b []byte) error {
@@ -2470,7 +2586,7 @@ func (m *ListenRequest_Filter) Reset()         { *m = ListenRequest_Filter{} }
 func (m *ListenRequest_Filter) String() string { return proto.CompactTextString(m) }
 func (*ListenRequest_Filter) ProtoMessage()    {}
 func (*ListenRequest_Filter) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{42, 0}
+	return fileDescriptor_f2ba358bb2150022, []int{44, 0}
 }
 
 func (m *ListenRequest_Filter) XXX_Unmarshal(b []byte) error {
@@ -2526,7 +2642,7 @@ func (m *ListenReply) Reset()         { *m = ListenReply{} }
 func (m *ListenReply) String() string { return proto.CompactTextString(m) }
 func (*ListenReply) ProtoMessage()    {}
 func (*ListenReply) Descriptor() ([]byte, []int) {
-	return fileDescriptor_f2ba358bb2150022, []int{43}
+	return fileDescriptor_f2ba358bb2150022, []int{45}
 }
 
 func (m *ListenReply) XXX_Unmarshal(b []byte) error {
@@ -2606,6 +2722,8 @@ func init() {
 	proto.RegisterType((*ListCollectionsReply)(nil), "threads.pb.ListCollectionsReply")
 	proto.RegisterType((*CreateRequest)(nil), "threads.pb.CreateRequest")
 	proto.RegisterType((*CreateReply)(nil), "threads.pb.CreateReply")
+	proto.RegisterType((*VerifyRequest)(nil), "threads.pb.VerifyRequest")
+	proto.RegisterType((*VerifyReply)(nil), "threads.pb.VerifyReply")
 	proto.RegisterType((*SaveRequest)(nil), "threads.pb.SaveRequest")
 	proto.RegisterType((*SaveReply)(nil), "threads.pb.SaveReply")
 	proto.RegisterType((*DeleteRequest)(nil), "threads.pb.DeleteRequest")
@@ -2629,113 +2747,116 @@ func init() {
 func init() { proto.RegisterFile("threads.proto", fileDescriptor_f2ba358bb2150022) }
 
 var fileDescriptor_f2ba358bb2150022 = []byte{
-	// 1681 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x59, 0xcf, 0x6f, 0x1b, 0xc5,
-	0x17, 0xdf, 0xb5, 0x1d, 0xff, 0x78, 0xb6, 0x13, 0x77, 0x94, 0xd8, 0xce, 0xb6, 0x8a, 0xfc, 0x9d,
-	0x2f, 0x6d, 0x03, 0x05, 0x53, 0xb9, 0x14, 0x55, 0x54, 0x2a, 0xd8, 0xb1, 0x1b, 0x07, 0xa2, 0x52,
-	0x6d, 0xd2, 0x22, 0xa4, 0xa2, 0x76, 0xe3, 0x1d, 0x27, 0x4b, 0x1c, 0xdb, 0xdd, 0xdd, 0xb4, 0xcd,
-	0x8d, 0x5b, 0xf9, 0x3b, 0x38, 0x20, 0xee, 0x9c, 0x38, 0x23, 0x71, 0xe4, 0xc6, 0x81, 0xff, 0x06,
-	0xcd, 0xcc, 0xfe, 0x98, 0x5d, 0xcf, 0x3a, 0x21, 0x0d, 0x85, 0x9b, 0x67, 0xf6, 0xfd, 0x7e, 0x9f,
-	0x99, 0xf7, 0xe6, 0x19, 0xca, 0xee, 0x81, 0x4d, 0x0c, 0xd3, 0x69, 0x4e, 0xed, 0x89, 0x3b, 0x41,
-	0x10, 0x2c, 0xf7, 0xf0, 0x43, 0x58, 0xda, 0x24, 0xee, 0xee, 0xe4, 0x90, 0x8c, 0x75, 0xf2, 0xfc,
-	0x98, 0x38, 0x2e, 0x42, 0x90, 0x3e, 0x24, 0x27, 0x75, 0xb5, 0xa1, 0xae, 0x17, 0xfa, 0x8a, 0x4e,
-	0x17, 0x68, 0x0d, 0x0a, 0x8e, 0xb5, 0x3f, 0x36, 0xdc, 0x63, 0x9b, 0xd4, 0x53, 0x0d, 0x75, 0xbd,
-	0xd4, 0x57, 0xf4, 0x70, 0xab, 0x53, 0x80, 0xdc, 0xd4, 0x38, 0x19, 0x4d, 0x0c, 0x13, 0xeb, 0x50,
-	0x0e, 0x25, 0x4e, 0x47, 0x8c, 0x77, 0x70, 0x60, 0x8c, 0x46, 0x64, 0xbc, 0x4f, 0x98, 0x54, 0xc6,
-	0x1b, 0x6c, 0xa1, 0x2a, 0x2c, 0xb8, 0x94, 0x9a, 0xc9, 0xa5, 0x1a, 0xf9, 0x52, 0x94, 0xf9, 0x02,
-	0x4a, 0x0f, 0xc8, 0xcb, 0x6e, 0x27, 0x34, 0x31, 0x63, 0xee, 0x6d, 0x75, 0xb9, 0x34, 0x9d, 0xfd,
-	0x46, 0xf7, 0xa0, 0x38, 0x98, 0x8c, 0x46, 0x64, 0xe0, 0x5a, 0x93, 0xb1, 0x53, 0x4f, 0x35, 0xd2,
-	0xeb, 0xc5, 0xd6, 0x95, 0x66, 0xe8, 0x6b, 0x73, 0x23, 0xf8, 0xbc, 0x31, 0x19, 0x0f, 0xad, 0x7d,
-	0x5d, 0x64, 0xa0, 0x32, 0xc7, 0xc6, 0x11, 0xa9, 0xa7, 0xa9, 0x15, 0x3a, 0xfb, 0x8d, 0x7f, 0x54,
-	0x61, 0x99, 0x29, 0xbe, 0x6f, 0x4f, 0x8e, 0xda, 0xa6, 0x69, 0x0b, 0x06, 0x18, 0xa6, 0x69, 0xfb,
-	0x06, 0xd0, 0xdf, 0xa8, 0xc2, 0xe3, 0xc6, 0xa2, 0xc3, 0xa3, 0x16, 0x33, 0x29, 0x7d, 0x5e, 0x93,
-	0x32, 0xa1, 0x49, 0x68, 0x19, 0x16, 0xf6, 0x46, 0x93, 0xc1, 0x61, 0x7d, 0xa1, 0xa1, 0xae, 0xe7,
-	0x75, 0xbe, 0xc0, 0x3f, 0xab, 0x50, 0x89, 0xcb, 0x0a, 0xd8, 0x55, 0x81, 0xbd, 0x0a, 0x59, 0x67,
-	0x70, 0x40, 0x8e, 0x0c, 0xcf, 0x4e, 0x6f, 0x85, 0x6e, 0x40, 0xce, 0x1a, 0x9b, 0xe4, 0x15, 0xf1,
-	0xcd, 0xbc, 0x24, 0x9a, 0xb9, 0x45, 0x3f, 0xe9, 0x3e, 0x05, 0xba, 0x06, 0x8b, 0x2f, 0x6d, 0xcb,
-	0x25, 0x8f, 0x8d, 0x91, 0x65, 0x1a, 0xee, 0xc4, 0xf6, 0x2c, 0x8c, 0xed, 0xa2, 0x35, 0x00, 0x2a,
-	0xe2, 0xbe, 0x35, 0x72, 0x89, 0xcd, 0x0c, 0x2e, 0xe8, 0xc2, 0x0e, 0xbe, 0x05, 0x0b, 0x4c, 0x32,
-	0xb5, 0x74, 0x6a, 0xb8, 0x07, 0xbe, 0xa5, 0xf4, 0x37, 0xb5, 0xf4, 0x78, 0x6c, 0x3d, 0x3f, 0xe6,
-	0x78, 0xcb, 0xeb, 0xde, 0x0a, 0x97, 0x00, 0x3c, 0x2c, 0x4c, 0x47, 0x27, 0xb8, 0x02, 0x8b, 0xdb,
-	0x96, 0xe3, 0x76, 0x3b, 0x8e, 0x97, 0x1a, 0xfc, 0x5a, 0x85, 0x52, 0xb0, 0x45, 0xf1, 0xf7, 0x01,
-	0xa4, 0xcd, 0x3d, 0xa7, 0xae, 0x32, 0xb7, 0x2e, 0x8b, 0x6e, 0x89, 0x64, 0xcd, 0x6e, 0x47, 0xa7,
-	0x74, 0x5a, 0x1f, 0x52, 0xdd, 0x8e, 0x14, 0x61, 0x4d, 0xc8, 0x58, 0xe3, 0xe1, 0x84, 0xd9, 0x53,
-	0x6c, 0x69, 0xa2, 0xa4, 0x4d, 0xe2, 0x76, 0x3b, 0x5b, 0xe3, 0xe1, 0x84, 0xc9, 0xd2, 0x19, 0x1d,
-	0xbe, 0x06, 0x15, 0x61, 0x3f, 0x11, 0xb9, 0x78, 0x1b, 0x16, 0xa3, 0xfc, 0x34, 0xc9, 0x14, 0x52,
-	0xdc, 0xe8, 0x92, 0xce, 0x17, 0x12, 0x80, 0xc9, 0x30, 0x7b, 0x15, 0x96, 0xba, 0x64, 0x44, 0x5c,
-	0x32, 0xf7, 0xb8, 0xe0, 0x25, 0x28, 0x87, 0x64, 0x34, 0x92, 0xcf, 0x18, 0xd4, 0x43, 0x10, 0xcd,
-	0x3b, 0x6b, 0x1f, 0x41, 0x76, 0xc0, 0x30, 0xe6, 0xc5, 0x62, 0x3e, 0xa6, 0x3d, 0x5a, 0xbc, 0x0c,
-	0x28, 0xa6, 0x81, 0xea, 0x1d, 0x40, 0xed, 0xd1, 0xd4, 0x34, 0x5c, 0xf2, 0x4f, 0xaa, 0xae, 0xc1,
-	0xca, 0xac, 0x12, 0xaa, 0xbd, 0x0d, 0x35, 0x1e, 0x86, 0xb3, 0x69, 0xf7, 0x03, 0x9e, 0x12, 0x02,
-	0x5e, 0x83, 0x95, 0x59, 0x11, 0x54, 0x76, 0x07, 0xea, 0x9b, 0xc4, 0x0d, 0x77, 0x4f, 0xc1, 0x81,
-	0x54, 0xf8, 0x2f, 0x2a, 0x54, 0x25, 0x42, 0x28, 0x48, 0xfe, 0xf3, 0xc7, 0xbb, 0x07, 0x97, 0x63,
-	0xa6, 0x33, 0xf9, 0x7f, 0x37, 0x04, 0x7d, 0x58, 0x95, 0x8b, 0xa1, 0x41, 0x10, 0x1c, 0x53, 0x4f,
-	0x73, 0x0c, 0xbf, 0x0f, 0x55, 0x7a, 0xe4, 0x43, 0x51, 0xf3, 0x6c, 0xc1, 0x4f, 0x60, 0x79, 0x86,
-	0x9a, 0xaa, 0xec, 0x46, 0x6f, 0x75, 0xae, 0x16, 0xc7, 0x6e, 0x03, 0x49, 0xc2, 0x22, 0x77, 0x3b,
-	0xb6, 0xa0, 0xbc, 0x61, 0x13, 0xc3, 0x25, 0xf3, 0xc2, 0x71, 0x0d, 0x16, 0x43, 0x9e, 0x07, 0x61,
-	0x60, 0x62, 0xbb, 0xe8, 0x0a, 0x14, 0xac, 0xb1, 0xe3, 0x1a, 0xe3, 0x81, 0x97, 0xe0, 0x92, 0x1e,
-	0x6e, 0xe0, 0x0f, 0xa1, 0xe8, 0xab, 0xa2, 0xf6, 0x37, 0xa0, 0xe8, 0x7f, 0xdb, 0xea, 0x72, 0xfb,
-	0x0b, 0xba, 0xb8, 0x85, 0xf7, 0xa1, 0xb8, 0x63, 0xbc, 0x78, 0x0b, 0x96, 0x15, 0xa1, 0xc0, 0x15,
-	0xd1, 0xe3, 0x72, 0xe4, 0xdf, 0x48, 0x17, 0xa1, 0x37, 0xe6, 0x64, 0x7a, 0xd6, 0xc9, 0x32, 0x14,
-	0x7d, 0x75, 0x54, 0xfb, 0xb7, 0x00, 0x7d, 0xc3, 0x79, 0x3b, 0xaa, 0x31, 0xe4, 0x99, 0x2e, 0x9a,
-	0x8d, 0x2a, 0x64, 0xc9, 0x2b, 0xcb, 0x71, 0x1d, 0xa6, 0x2b, 0xaf, 0x7b, 0x2b, 0x9a, 0x83, 0xfb,
-	0xd6, 0xd8, 0xbc, 0xa0, 0x1c, 0x3c, 0x3f, 0x26, 0xf6, 0xc9, 0xe7, 0x3b, 0x5f, 0x3e, 0x60, 0xa5,
-	0xa2, 0xa4, 0x87, 0x1b, 0xf8, 0x5d, 0x28, 0x70, 0x45, 0xd4, 0x9a, 0x48, 0xba, 0xd4, 0x78, 0xba,
-	0x8e, 0x60, 0x89, 0x92, 0x76, 0x4e, 0xb6, 0xba, 0x17, 0x61, 0xd7, 0x1a, 0x40, 0x18, 0x15, 0xaf,
-	0x86, 0x09, 0x3b, 0xf8, 0x06, 0x94, 0x43, 0x75, 0xd4, 0x3a, 0x0d, 0xf2, 0xfe, 0x67, 0x4f, 0x61,
-	0xb0, 0xc6, 0x8f, 0xa0, 0xb6, 0xe3, 0x1a, 0xb6, 0xbb, 0x6b, 0x1b, 0x63, 0xc7, 0x38, 0xf5, 0x22,
-	0x3f, 0xa3, 0x8d, 0xf8, 0xd7, 0x14, 0x54, 0x75, 0x62, 0x98, 0x12, 0xb1, 0x4f, 0xa1, 0xe6, 0xc8,
-	0x35, 0x32, 0x4d, 0xc5, 0xd6, 0xff, 0xc5, 0x3b, 0x21, 0xc1, 0xb8, 0xbe, 0xa2, 0x27, 0x49, 0x41,
-	0x77, 0x00, 0x0e, 0x02, 0x48, 0x7a, 0xe5, 0xae, 0x2a, 0xca, 0x0c, 0x01, 0xdb, 0x57, 0x74, 0x81,
-	0x16, 0xdd, 0x85, 0xe2, 0x30, 0x04, 0x0f, 0x0b, 0x6d, 0xb1, 0x55, 0x13, 0x59, 0x05, 0x6c, 0xf5,
-	0x15, 0x5d, 0xa4, 0x46, 0x9b, 0xb0, 0x34, 0x8c, 0x66, 0x99, 0xdd, 0xff, 0xb1, 0xde, 0x29, 0x06,
-	0x84, 0xbe, 0xa2, 0xc7, 0xb9, 0x3a, 0x79, 0xc8, 0x4e, 0xa6, 0xd4, 0x21, 0xfc, 0xbb, 0x0a, 0xcb,
-	0x33, 0x51, 0xa4, 0x19, 0x6d, 0x41, 0xfe, 0xc0, 0x3b, 0x09, 0x5e, 0xd0, 0x96, 0x67, 0x1c, 0x9c,
-	0x8e, 0x4e, 0xfa, 0x8a, 0x1e, 0xd0, 0xa1, 0xdb, 0x50, 0x18, 0xfa, 0x80, 0xf5, 0xa2, 0xb2, 0x32,
-	0xeb, 0x1a, 0xe7, 0x0a, 0x29, 0x51, 0x1b, 0xca, 0x43, 0x11, 0x4d, 0x5e, 0x54, 0x56, 0xe5, 0x4e,
-	0x71, 0xf6, 0x28, 0x87, 0xe0, 0xd0, 0xeb, 0x0c, 0xd4, 0xbe, 0xa2, 0xd5, 0xf0, 0xdf, 0xc0, 0x45,
-	0x1b, 0xca, 0x03, 0xb1, 0x74, 0x78, 0x41, 0x88, 0x78, 0x12, 0xa9, 0x2d, 0xd4, 0x93, 0x08, 0x07,
-	0x05, 0x88, 0x13, 0xde, 0xf0, 0x32, 0x80, 0x08, 0x05, 0x80, 0x02, 0x44, 0xa0, 0xa6, 0xfa, 0x4d,
-	0xf1, 0xa2, 0xf6, 0xe0, 0x11, 0xd1, 0x1f, 0xb9, 0xc9, 0xa9, 0xfe, 0x08, 0x47, 0x0c, 0xda, 0x0b,
-	0xe7, 0x87, 0x76, 0xf6, 0x4d, 0xa1, 0x9d, 0x7b, 0x43, 0x68, 0x7f, 0x97, 0x86, 0x95, 0x59, 0x24,
-	0x50, 0xc0, 0xdd, 0x85, 0xe2, 0x20, 0x2c, 0xbb, 0x5e, 0xee, 0x6b, 0xb2, 0x24, 0x71, 0xb0, 0x89,
-	0xd4, 0x14, 0xe4, 0x8e, 0x5f, 0x19, 0x65, 0x20, 0x0f, 0xca, 0x26, 0x7b, 0x87, 0xfb, 0x0b, 0xaa,
-	0xd3, 0x0c, 0x8b, 0x9a, 0x2c, 0xaf, 0x42, 0xcd, 0xa3, 0x3a, 0x05, 0xea, 0xc8, 0x61, 0xcc, 0x9c,
-	0xe7, 0x30, 0x2e, 0x9c, 0xff, 0x30, 0x66, 0xdf, 0xe0, 0x30, 0xfe, 0x94, 0x82, 0x32, 0xed, 0xd4,
-	0xc8, 0xdc, 0x1b, 0xff, 0x13, 0xc8, 0x0d, 0x59, 0x5f, 0xea, 0xcf, 0x06, 0x1a, 0xf1, 0xa7, 0x60,
-	0xc0, 0xdf, 0xe4, 0x0d, 0xac, 0xee, 0x33, 0x68, 0xbf, 0xa9, 0x90, 0xe5, 0x7b, 0x92, 0xc2, 0xa1,
-	0x9e, 0xa1, 0xb8, 0xa5, 0xe2, 0xc5, 0x0d, 0x7d, 0x0a, 0x59, 0x0e, 0x16, 0x96, 0xa4, 0xc5, 0xd6,
-	0xf5, 0xd3, 0xac, 0x69, 0xb6, 0x39, 0xb6, 0x3c, 0x36, 0x7c, 0x0b, 0xb2, 0x7c, 0x07, 0xe5, 0x20,
-	0xdd, 0xde, 0xde, 0xae, 0x28, 0x08, 0x20, 0xbb, 0xa1, 0xf7, 0xda, 0xbb, 0xbd, 0x8a, 0x8a, 0xf2,
-	0x90, 0xd9, 0x69, 0x3f, 0xee, 0x55, 0x52, 0x74, 0xb7, 0xdb, 0xdb, 0xee, 0xed, 0xf6, 0x2a, 0x69,
-	0xfc, 0x87, 0x0a, 0x45, 0x5f, 0x38, 0xcd, 0xc3, 0x45, 0x79, 0xf3, 0x71, 0xcc, 0x9b, 0x35, 0x99,
-	0x37, 0xf4, 0x95, 0x1d, 0x75, 0x22, 0x52, 0xd1, 0x33, 0xb1, 0x8a, 0xfe, 0x5e, 0xe0, 0x60, 0xe8,
-	0x97, 0x12, 0xf8, 0xa5, 0x0a, 0x7e, 0xa5, 0x5a, 0x7f, 0x96, 0x20, 0xdd, 0x7e, 0xb8, 0x85, 0xfa,
-	0x90, 0xf7, 0x87, 0x4f, 0xe8, 0x72, 0xac, 0x25, 0x17, 0x87, 0x5c, 0xda, 0xaa, 0xfc, 0x23, 0xed,
-	0x04, 0x95, 0x75, 0xf5, 0xa6, 0x8a, 0xee, 0xc2, 0x02, 0x1b, 0x33, 0xa0, 0xba, 0x48, 0x29, 0x4e,
-	0xa1, 0xb4, 0xaa, 0xe4, 0x0b, 0x13, 0x80, 0xbe, 0x80, 0x72, 0x64, 0x6c, 0x84, 0x1a, 0x33, 0xa4,
-	0xb1, 0x89, 0xd2, 0x1c, 0x61, 0x6d, 0xc8, 0x79, 0x83, 0x0a, 0xa4, 0x49, 0xa7, 0x17, 0x5c, 0x40,
-	0x3d, 0x69, 0xb2, 0x81, 0x15, 0xb4, 0x09, 0x85, 0x60, 0xc2, 0x80, 0xae, 0x24, 0x0c, 0x2e, 0xb8,
-	0x98, 0x39, 0x63, 0x0d, 0xac, 0xa0, 0x2e, 0xe4, 0xfd, 0xa9, 0x41, 0x34, 0xbe, 0xb1, 0x91, 0x83,
-	0xb6, 0x2a, 0xff, 0xc8, 0xa5, 0xec, 0xb0, 0xf0, 0x84, 0x4f, 0xa4, 0x99, 0xf0, 0xcc, 0x3c, 0xc6,
-	0xb5, 0xb5, 0x39, 0x14, 0x5c, 0xe8, 0x13, 0xa8, 0xc4, 0x9f, 0xf8, 0x28, 0x52, 0x69, 0x13, 0xa6,
-	0x0c, 0xda, 0xff, 0xe6, 0x13, 0x05, 0xd2, 0xe3, 0x8f, 0xfc, 0xa8, 0xf4, 0x84, 0x29, 0x42, 0x54,
-	0xba, 0x7c, 0x4e, 0xa0, 0xa0, 0xa7, 0x70, 0x69, 0xe6, 0xcd, 0x88, 0xde, 0x39, 0xe5, 0x49, 0xc9,
-	0xe5, 0x9f, 0xe1, 0xe1, 0x89, 0x15, 0x74, 0x08, 0xcb, 0xb2, 0x37, 0x34, 0xba, 0x3e, 0x87, 0x5b,
-	0x7c, 0xac, 0x6b, 0x57, 0x4f, 0x27, 0xa4, 0x9a, 0xd2, 0xdf, 0xa7, 0x54, 0xf4, 0x35, 0x2c, 0xc5,
-	0x1e, 0xce, 0x08, 0xc7, 0xc1, 0x39, 0xfb, 0x06, 0xd7, 0x1a, 0x73, 0x69, 0xb8, 0x1f, 0xf7, 0x20,
-	0xcb, 0x8b, 0x26, 0x4a, 0xee, 0x76, 0xb4, 0xa4, 0x1a, 0x8b, 0x15, 0x74, 0x07, 0x32, 0xb4, 0x72,
-	0xa2, 0xa4, 0x56, 0x47, 0x93, 0x17, 0x59, 0xae, 0x99, 0x67, 0x0f, 0x25, 0xf7, 0x39, 0x5a, 0x52,
-	0xa5, 0xc5, 0x0a, 0xba, 0x0d, 0xe9, 0xbe, 0xe1, 0xa0, 0x84, 0x26, 0x47, 0x93, 0x56, 0x5a, 0x6e,
-	0x30, 0xad, 0x83, 0x28, 0xa9, 0xc3, 0xd1, 0xe4, 0xd5, 0x96, 0x1f, 0x55, 0xbf, 0x82, 0xa2, 0x79,
-	0xed, 0x8d, 0x96, 0x5c, 0x74, 0xb1, 0x82, 0xbe, 0x81, 0xa5, 0x58, 0xe3, 0x1e, 0xcd, 0xa5, 0xfc,
-	0x6d, 0x14, 0xcd, 0xa5, 0xac, 0xf3, 0xf7, 0x6e, 0xd9, 0x67, 0x50, 0x89, 0x37, 0x4f, 0xd1, 0x63,
-	0x95, 0xd0, 0x64, 0x47, 0x8f, 0x95, 0xb4, 0xff, 0xf2, 0x34, 0x7c, 0x06, 0x59, 0x5e, 0x7f, 0xa2,
-	0x79, 0x8b, 0x54, 0xd8, 0x68, 0xde, 0x84, 0x72, 0x85, 0x95, 0x9b, 0x6a, 0xa7, 0x09, 0x35, 0x6b,
-	0xd2, 0x74, 0xc9, 0x2b, 0xd7, 0x1a, 0x11, 0x9f, 0xf0, 0xe9, 0xbe, 0x3d, 0x1d, 0x74, 0x72, 0xbb,
-	0x7c, 0xf5, 0x50, 0xfd, 0x21, 0x95, 0xdb, 0xed, 0xeb, 0xbd, 0x76, 0x77, 0x67, 0x2f, 0xcb, 0xfe,
-	0x65, 0xb9, 0xf5, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0xde, 0xd3, 0xda, 0x76, 0x19, 0x00,
-	0x00,
+	// 1738 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x59, 0x4d, 0x73, 0xdb, 0x44,
+	0x18, 0x96, 0x6c, 0xc7, 0x1f, 0xaf, 0xe3, 0xc4, 0xdd, 0x49, 0x6c, 0x47, 0xed, 0x64, 0xcc, 0x42,
+	0xdb, 0x40, 0xc1, 0x74, 0x52, 0xca, 0x74, 0xe8, 0x4c, 0xc1, 0x8e, 0xdd, 0x38, 0x90, 0x29, 0x1d,
+	0x25, 0x2d, 0xc3, 0x4c, 0x99, 0x56, 0xb1, 0xd6, 0x89, 0x88, 0x63, 0xbb, 0x92, 0x92, 0xd6, 0xbf,
+	0x00, 0xfe, 0x02, 0x57, 0x0e, 0x0c, 0x77, 0x4e, 0x1c, 0x38, 0x31, 0xc3, 0x81, 0x03, 0x37, 0xfe,
+	0x0f, 0xb3, 0xbb, 0x92, 0xb5, 0x92, 0x56, 0x4e, 0x48, 0xd3, 0xc2, 0xcd, 0xbb, 0x7a, 0xbf, 0xf7,
+	0xd9, 0x7d, 0x76, 0x5f, 0x43, 0xc9, 0x3d, 0xb0, 0x89, 0x61, 0x3a, 0x8d, 0xb1, 0x3d, 0x72, 0x47,
+	0x08, 0xa6, 0xc3, 0x3d, 0xfc, 0x10, 0x16, 0x37, 0x89, 0xbb, 0x3b, 0x3a, 0x24, 0x43, 0x9d, 0x3c,
+	0x3f, 0x26, 0x8e, 0x8b, 0x10, 0xa4, 0x0f, 0xc9, 0xa4, 0xa6, 0xd6, 0xd5, 0xb5, 0x42, 0x57, 0xd1,
+	0xe9, 0x00, 0xad, 0x42, 0xc1, 0xb1, 0xf6, 0x87, 0x86, 0x7b, 0x6c, 0x93, 0x5a, 0xaa, 0xae, 0xae,
+	0xcd, 0x77, 0x15, 0x3d, 0x98, 0x6a, 0x15, 0x20, 0x37, 0x36, 0x26, 0x83, 0x91, 0x61, 0x62, 0x1d,
+	0x4a, 0x81, 0xc5, 0xf1, 0x80, 0xe9, 0xf6, 0x0e, 0x8c, 0xc1, 0x80, 0x0c, 0xf7, 0x09, 0xb3, 0xca,
+	0x74, 0xa7, 0x53, 0xa8, 0x02, 0x73, 0x2e, 0x95, 0x66, 0x76, 0xa9, 0x47, 0x3e, 0x14, 0x6d, 0x9e,
+	0xc0, 0xfc, 0x03, 0xf2, 0xa2, 0xdd, 0x0a, 0x42, 0xcc, 0x98, 0x7b, 0x5b, 0x6d, 0x6e, 0x4d, 0x67,
+	0xbf, 0xd1, 0x3d, 0x28, 0xf6, 0x46, 0x83, 0x01, 0xe9, 0xb9, 0xd6, 0x68, 0xe8, 0xd4, 0x52, 0xf5,
+	0xf4, 0x5a, 0x71, 0xfd, 0x4a, 0x23, 0xc8, 0xb5, 0xb1, 0x31, 0xfd, 0xbc, 0x31, 0x1a, 0xf6, 0xad,
+	0x7d, 0x5d, 0x54, 0xa0, 0x36, 0x87, 0xc6, 0x11, 0xa9, 0xa5, 0x69, 0x14, 0x3a, 0xfb, 0x8d, 0x7f,
+	0x52, 0x61, 0x89, 0x39, 0xbe, 0x6f, 0x8f, 0x8e, 0x9a, 0xa6, 0x69, 0x0b, 0x01, 0x18, 0xa6, 0x69,
+	0xfb, 0x01, 0xd0, 0xdf, 0xa8, 0xcc, 0xeb, 0xc6, 0xaa, 0xc3, 0xab, 0x16, 0x09, 0x29, 0x7d, 0xde,
+	0x90, 0x32, 0x41, 0x48, 0x68, 0x09, 0xe6, 0xf6, 0x06, 0xa3, 0xde, 0x61, 0x6d, 0xae, 0xae, 0xae,
+	0xe5, 0x75, 0x3e, 0xc0, 0xbf, 0xa8, 0x50, 0x8e, 0xda, 0x9a, 0xaa, 0xab, 0x82, 0x7a, 0x05, 0xb2,
+	0x4e, 0xef, 0x80, 0x1c, 0x19, 0x5e, 0x9c, 0xde, 0x08, 0xdd, 0x80, 0x9c, 0x35, 0x34, 0xc9, 0x4b,
+	0xe2, 0x87, 0x79, 0x49, 0x0c, 0x73, 0x8b, 0x7e, 0xd2, 0x7d, 0x09, 0x74, 0x0d, 0x16, 0x5e, 0xd8,
+	0x96, 0x4b, 0x1e, 0x1b, 0x03, 0xcb, 0x34, 0xdc, 0x91, 0xed, 0x45, 0x18, 0x99, 0x45, 0xab, 0x00,
+	0xd4, 0xc4, 0x7d, 0x6b, 0xe0, 0x12, 0x9b, 0x05, 0x5c, 0xd0, 0x85, 0x19, 0x7c, 0x0b, 0xe6, 0x98,
+	0x65, 0x1a, 0xe9, 0xd8, 0x70, 0x0f, 0xfc, 0x48, 0xe9, 0x6f, 0x1a, 0xe9, 0xf1, 0xd0, 0x7a, 0x7e,
+	0xcc, 0xf1, 0x96, 0xd7, 0xbd, 0x11, 0x9e, 0x07, 0xf0, 0xb0, 0x30, 0x1e, 0x4c, 0x70, 0x19, 0x16,
+	0xb6, 0x2d, 0xc7, 0x6d, 0xb7, 0x1c, 0x6f, 0x69, 0xf0, 0x77, 0x2a, 0xcc, 0x4f, 0xa7, 0x28, 0xfe,
+	0x3e, 0x80, 0xb4, 0xb9, 0xe7, 0xd4, 0x54, 0x96, 0xd6, 0x65, 0x31, 0x2d, 0x51, 0xac, 0xd1, 0x6e,
+	0xe9, 0x54, 0x4e, 0xeb, 0x42, 0xaa, 0xdd, 0x92, 0x22, 0xac, 0x01, 0x19, 0x6b, 0xd8, 0x1f, 0xb1,
+	0x78, 0x8a, 0xeb, 0x9a, 0x68, 0x69, 0x93, 0xb8, 0xed, 0xd6, 0xd6, 0xb0, 0x3f, 0x62, 0xb6, 0x74,
+	0x26, 0x87, 0xaf, 0x41, 0x59, 0x98, 0x4f, 0x44, 0x2e, 0xde, 0x86, 0x85, 0xb0, 0x3e, 0x5d, 0x64,
+	0x0a, 0x29, 0x1e, 0xf4, 0xbc, 0xce, 0x07, 0x12, 0x80, 0xc9, 0x30, 0x7b, 0x15, 0x16, 0xdb, 0x64,
+	0x40, 0x5c, 0x32, 0x73, 0xbb, 0xe0, 0x45, 0x28, 0x05, 0x62, 0xb4, 0x92, 0xcf, 0x18, 0xd4, 0x03,
+	0x10, 0xcd, 0xda, 0x6b, 0x1f, 0x41, 0xb6, 0xc7, 0x30, 0xe6, 0xd5, 0x62, 0x36, 0xa6, 0x3d, 0x59,
+	0xbc, 0x04, 0x28, 0xe2, 0x81, 0xfa, 0xed, 0x41, 0xf5, 0xd1, 0xd8, 0x34, 0x5c, 0xf2, 0x3a, 0x5d,
+	0x57, 0x61, 0x39, 0xee, 0x84, 0x7a, 0x6f, 0x42, 0x95, 0x97, 0xe1, 0x6c, 0xde, 0xfd, 0x82, 0xa7,
+	0x84, 0x82, 0x57, 0x61, 0x39, 0x6e, 0x82, 0xda, 0x6e, 0x41, 0x6d, 0x93, 0xb8, 0xc1, 0xec, 0x29,
+	0x38, 0x90, 0x1a, 0xff, 0x55, 0x85, 0x8a, 0xc4, 0x08, 0x05, 0xc9, 0xff, 0x7e, 0x7b, 0x77, 0xe0,
+	0x72, 0x24, 0x74, 0x66, 0xff, 0xdf, 0x96, 0xa0, 0x0b, 0x2b, 0x72, 0x33, 0xb4, 0x08, 0x42, 0x62,
+	0xea, 0x69, 0x89, 0xe1, 0xf7, 0xa1, 0x42, 0xb7, 0x7c, 0x60, 0x6a, 0x56, 0x2c, 0xf8, 0x09, 0x2c,
+	0xc5, 0xa4, 0xa9, 0xcb, 0x76, 0xf8, 0x54, 0xe7, 0x6e, 0x71, 0xe4, 0x34, 0x90, 0x2c, 0x58, 0xe8,
+	0x6c, 0xc7, 0x16, 0x94, 0x36, 0x6c, 0x62, 0xb8, 0x64, 0x56, 0x39, 0xae, 0xc1, 0x42, 0xa0, 0xf3,
+	0x20, 0x28, 0x4c, 0x64, 0x16, 0x5d, 0x81, 0x82, 0x35, 0x74, 0x5c, 0x63, 0xd8, 0xf3, 0x16, 0x78,
+	0x5e, 0x0f, 0x26, 0xf0, 0x87, 0x50, 0xf4, 0x5d, 0xd1, 0xf8, 0xeb, 0x50, 0xf4, 0xbf, 0x6d, 0xb5,
+	0x79, 0xfc, 0x05, 0x5d, 0x9c, 0xa2, 0xb1, 0x3d, 0x26, 0xb6, 0xd5, 0x9f, 0xbc, 0xfe, 0xd8, 0x4a,
+	0x50, 0xf4, 0x5d, 0xd1, 0x2d, 0xb3, 0x0f, 0xc5, 0x1d, 0xe3, 0xe4, 0x0d, 0xd4, 0xa4, 0x08, 0x05,
+	0xee, 0x88, 0x7a, 0x3d, 0xf2, 0xcf, 0xc2, 0x8b, 0xf0, 0x1b, 0x29, 0x6f, 0x3a, 0x5e, 0xde, 0x12,
+	0x14, 0x7d, 0x77, 0xd4, 0xfb, 0xb7, 0x00, 0x5d, 0xc3, 0x79, 0x33, 0xae, 0x31, 0xe4, 0x99, 0x2f,
+	0x8a, 0x83, 0x0a, 0x64, 0xc9, 0x4b, 0xcb, 0x71, 0x1d, 0xe6, 0x2b, 0xaf, 0x7b, 0x23, 0xba, 0x06,
+	0xf7, 0xad, 0xa1, 0x79, 0x41, 0x6b, 0xf0, 0xfc, 0x98, 0xd8, 0x93, 0xcf, 0x77, 0xbe, 0x7c, 0xc0,
+	0x48, 0x6a, 0x5e, 0x0f, 0x26, 0xf0, 0xbb, 0x50, 0xe0, 0x8e, 0x68, 0x34, 0xa1, 0xe5, 0x52, 0xa3,
+	0xcb, 0x75, 0x04, 0x8b, 0x54, 0xb4, 0x35, 0xd9, 0x6a, 0x5f, 0x44, 0x5c, 0xab, 0x00, 0x41, 0x55,
+	0x3c, 0xf6, 0x14, 0x66, 0xf0, 0x0d, 0x28, 0x05, 0xee, 0x68, 0x74, 0x1a, 0xe4, 0xfd, 0xcf, 0x9e,
+	0xc3, 0xe9, 0x18, 0x3f, 0x82, 0xea, 0x8e, 0x6b, 0xd8, 0xee, 0xae, 0x6d, 0x0c, 0x1d, 0xe3, 0x54,
+	0x0a, 0x39, 0x63, 0x8c, 0xf8, 0xf7, 0x14, 0x54, 0x74, 0x62, 0x98, 0x12, 0xb3, 0x4f, 0xa1, 0xea,
+	0xc8, 0x3d, 0x32, 0x4f, 0xc5, 0xf5, 0xb7, 0xc5, 0xd3, 0x28, 0x21, 0xb8, 0xae, 0xa2, 0x27, 0x59,
+	0x41, 0x77, 0x00, 0x0e, 0xa6, 0x90, 0xf4, 0x88, 0xb6, 0x22, 0xda, 0x0c, 0x00, 0xdb, 0x55, 0x74,
+	0x41, 0x16, 0xdd, 0x85, 0x62, 0x3f, 0x00, 0x0f, 0x2b, 0x6d, 0x71, 0xbd, 0x2a, 0xaa, 0x0a, 0xd8,
+	0xea, 0x2a, 0xba, 0x28, 0x8d, 0x36, 0x61, 0xb1, 0x1f, 0x5e, 0x65, 0xc6, 0x3c, 0x91, 0x5b, 0x5b,
+	0x04, 0x08, 0x5d, 0x45, 0x8f, 0x6a, 0xb5, 0xf2, 0x90, 0x1d, 0x8d, 0x69, 0x42, 0xf8, 0x2f, 0x15,
+	0x96, 0x62, 0x55, 0xa4, 0x2b, 0xba, 0x0e, 0xf9, 0x03, 0x6f, 0x27, 0x78, 0x45, 0x5b, 0x8a, 0x25,
+	0x38, 0x1e, 0x4c, 0xba, 0x8a, 0x3e, 0x95, 0x43, 0xb7, 0xa1, 0xd0, 0xf7, 0x01, 0xeb, 0x55, 0x65,
+	0x39, 0x9e, 0x1a, 0xd7, 0x0a, 0x24, 0x51, 0x13, 0x4a, 0x7d, 0x11, 0x4d, 0x5e, 0x55, 0x56, 0xe4,
+	0x49, 0x71, 0xf5, 0xb0, 0x86, 0x90, 0xd0, 0x9f, 0x19, 0xa8, 0x7e, 0x45, 0x79, 0xf8, 0xbf, 0xc0,
+	0x45, 0x13, 0x4a, 0x3d, 0x91, 0xb4, 0xbc, 0x22, 0x84, 0x32, 0x09, 0xb1, 0x1a, 0xcd, 0x24, 0xa4,
+	0x41, 0x4d, 0x9c, 0x88, 0xdc, 0x52, 0xcb, 0xc7, 0x4d, 0x84, 0xc8, 0x87, 0x9a, 0x08, 0x69, 0x50,
+	0x8c, 0x39, 0x01, 0x49, 0xc8, 0x30, 0x26, 0x70, 0x08, 0xc5, 0x98, 0x20, 0x4d, 0xfd, 0x9b, 0xe2,
+	0x59, 0xef, 0x21, 0x2c, 0xe4, 0x3f, 0x44, 0x06, 0xd4, 0x7f, 0x48, 0x23, 0xb2, 0x3b, 0xe6, 0xce,
+	0xbf, 0x3b, 0xb2, 0xaf, 0xba, 0x3b, 0x72, 0xaf, 0xb8, 0x3b, 0x7e, 0x4b, 0xc3, 0x72, 0x1c, 0x4c,
+	0x14, 0xb3, 0x77, 0xa1, 0xd8, 0x0b, 0xee, 0x0c, 0x1e, 0x7c, 0xaa, 0xb2, 0x75, 0xe6, 0x78, 0x15,
+	0xa5, 0xa9, 0xf2, 0x49, 0x40, 0xea, 0x5e, 0x94, 0x55, 0xd9, 0x0a, 0x7b, 0xca, 0x82, 0x34, 0xdd,
+	0x64, 0x8e, 0xcf, 0xcc, 0xb2, 0x4d, 0x36, 0xa5, 0x6d, 0xd6, 0x81, 0xf0, 0x07, 0xd4, 0xa7, 0x19,
+	0x90, 0xaa, 0x0c, 0x14, 0x02, 0xe7, 0x52, 0x9f, 0x82, 0x74, 0xe8, 0x30, 0xc8, 0x9c, 0xe7, 0x30,
+	0x98, 0x3b, 0xff, 0x61, 0x90, 0x7d, 0x85, 0xc3, 0xe0, 0xe7, 0x14, 0x94, 0xe8, 0x1d, 0x95, 0xcc,
+	0x64, 0x9c, 0x4f, 0x20, 0xd7, 0x67, 0x37, 0x72, 0xbf, 0x2b, 0x52, 0x8f, 0x3e, 0x82, 0xa7, 0xfa,
+	0x0d, 0x7e, 0x75, 0xd7, 0x7d, 0x05, 0xed, 0x0f, 0x15, 0xb2, 0x7c, 0x4e, 0x42, 0x5c, 0xea, 0x19,
+	0xc8, 0x35, 0x15, 0x25, 0x57, 0xf4, 0x29, 0x64, 0x39, 0xd2, 0xd8, 0x22, 0x2d, 0xac, 0x5f, 0x3f,
+	0x2d, 0x9a, 0x46, 0x93, 0x03, 0xd3, 0x53, 0xc3, 0xb7, 0x20, 0xcb, 0x67, 0x50, 0x0e, 0xd2, 0xcd,
+	0xed, 0xed, 0xb2, 0x82, 0x00, 0xb2, 0x1b, 0x7a, 0xa7, 0xb9, 0xdb, 0x29, 0xab, 0x28, 0x0f, 0x99,
+	0x9d, 0xe6, 0xe3, 0x4e, 0x39, 0x45, 0x67, 0xdb, 0x9d, 0xed, 0xce, 0x6e, 0xa7, 0x9c, 0xc6, 0x7f,
+	0xab, 0x50, 0xf4, 0x8d, 0xd3, 0x75, 0xb8, 0xa8, 0x6c, 0x3e, 0x8e, 0x64, 0xb3, 0x2a, 0xcb, 0x66,
+	0x3c, 0x98, 0x44, 0x92, 0x08, 0xdd, 0x28, 0x32, 0x91, 0x1b, 0xc5, 0x7b, 0xd3, 0x04, 0x83, 0xbc,
+	0x94, 0x69, 0x5e, 0xaa, 0x90, 0x57, 0x6a, 0xfd, 0x87, 0x12, 0xa4, 0x9b, 0x0f, 0xb7, 0x50, 0x17,
+	0xf2, 0x7e, 0xdb, 0x0d, 0x5d, 0x8e, 0x3c, 0x46, 0xc4, 0xf6, 0x9e, 0xb6, 0x22, 0xff, 0x48, 0x6f,
+	0xa2, 0xca, 0x9a, 0x7a, 0x53, 0x45, 0x77, 0x61, 0x8e, 0x35, 0x58, 0x50, 0x4d, 0x94, 0x14, 0xfb,
+	0x6f, 0x5a, 0x45, 0xf2, 0x85, 0x19, 0x40, 0x5f, 0x40, 0x29, 0xd4, 0x30, 0x43, 0xf5, 0x98, 0x68,
+	0xa4, 0x97, 0x36, 0xc3, 0x58, 0x13, 0x72, 0x5e, 0x8b, 0x06, 0x69, 0xd2, 0xbe, 0x0d, 0x37, 0x50,
+	0x4b, 0xea, 0xe9, 0x60, 0x05, 0x6d, 0x42, 0x61, 0xda, 0x5b, 0x41, 0x57, 0x12, 0x5a, 0x36, 0xdc,
+	0xcc, 0x8c, 0x86, 0x0e, 0x56, 0x50, 0x1b, 0xf2, 0x7e, 0xbf, 0x24, 0x5c, 0xdf, 0x48, 0xb3, 0x45,
+	0x5b, 0x91, 0x7f, 0xe4, 0x56, 0x76, 0x58, 0x79, 0x82, 0xc7, 0x61, 0xac, 0x3c, 0xb1, 0x36, 0x84,
+	0xb6, 0x3a, 0x43, 0x82, 0x1b, 0x7d, 0x02, 0xe5, 0x68, 0x73, 0x03, 0x85, 0x98, 0x3e, 0xa1, 0xbf,
+	0xa2, 0xbd, 0x35, 0x5b, 0x68, 0x6a, 0x3d, 0xda, 0xde, 0x08, 0x5b, 0x4f, 0xe8, 0x9f, 0x84, 0xad,
+	0xcb, 0x3b, 0x24, 0x0a, 0x7a, 0x0a, 0x97, 0x62, 0xaf, 0x65, 0xf4, 0xce, 0x29, 0x8f, 0x69, 0x6e,
+	0xff, 0x0c, 0x4f, 0x6e, 0xac, 0xa0, 0x43, 0x58, 0x92, 0x75, 0x0f, 0xd0, 0xf5, 0x19, 0xda, 0x62,
+	0x9b, 0x42, 0xbb, 0x7a, 0xba, 0x20, 0xf5, 0x94, 0xfe, 0x3e, 0xa5, 0xa2, 0xaf, 0x61, 0x31, 0xd2,
+	0x32, 0x40, 0x38, 0x0a, 0xce, 0x78, 0xf7, 0x41, 0xab, 0xcf, 0x94, 0xe1, 0x79, 0xdc, 0x83, 0x2c,
+	0x67, 0x5c, 0x94, 0x7c, 0xdb, 0xd2, 0x92, 0x08, 0x9a, 0xeb, 0x73, 0xd2, 0x45, 0xc9, 0x57, 0x2d,
+	0x2d, 0x89, 0xa3, 0xb1, 0x82, 0xee, 0x40, 0x86, 0x32, 0x2f, 0x4a, 0xba, 0x67, 0x69, 0x72, 0x92,
+	0xe6, 0x9e, 0xf9, 0xea, 0xa3, 0xe4, 0x4b, 0x96, 0x96, 0xc4, 0xd4, 0x58, 0x41, 0xb7, 0x21, 0xdd,
+	0x35, 0x1c, 0x94, 0x70, 0xc3, 0xd2, 0xa4, 0x4c, 0xcd, 0x03, 0xa6, 0x3c, 0x8a, 0x92, 0xae, 0x57,
+	0x9a, 0x9c, 0xad, 0xf9, 0x56, 0xf7, 0x19, 0x18, 0xcd, 0xba, 0x5b, 0x69, 0xc9, 0xa4, 0x8d, 0x15,
+	0xf4, 0x0d, 0x2c, 0x46, 0x1e, 0x1e, 0x61, 0x2c, 0xc8, 0xdf, 0x76, 0x61, 0x2c, 0xc8, 0x5e, 0x2e,
+	0xde, 0x29, 0xfd, 0x0c, 0xca, 0xd1, 0x9b, 0x5b, 0x78, 0x5b, 0x26, 0x3c, 0x12, 0xc2, 0xdb, 0x52,
+	0x7a, 0xf9, 0xf3, 0x3c, 0x7c, 0x06, 0x59, 0xce, 0x5f, 0xe1, 0x75, 0x0b, 0x31, 0x74, 0x78, 0xdd,
+	0x04, 0xba, 0xc3, 0xca, 0x4d, 0xb5, 0xd5, 0x80, 0xaa, 0x35, 0x6a, 0xb8, 0xe4, 0xa5, 0x6b, 0x0d,
+	0x88, 0x2f, 0xf8, 0x74, 0xdf, 0x1e, 0xf7, 0x5a, 0xb9, 0x5d, 0x3e, 0x7a, 0xa8, 0xfe, 0x98, 0xca,
+	0xed, 0x76, 0xf5, 0x4e, 0xb3, 0xbd, 0xb3, 0x97, 0x65, 0xff, 0x4f, 0xdd, 0xfa, 0x27, 0x00, 0x00,
+	0xff, 0xff, 0xe6, 0x0e, 0x0b, 0x46, 0xb0, 0x1a, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -2763,6 +2884,7 @@ type APIClient interface {
 	GetCollectionIndexes(ctx context.Context, in *GetCollectionIndexesRequest, opts ...grpc.CallOption) (*GetCollectionIndexesReply, error)
 	ListCollections(ctx context.Context, in *ListCollectionsRequest, opts ...grpc.CallOption) (*ListCollectionsReply, error)
 	Create(ctx context.Context, in *CreateRequest, opts ...grpc.CallOption) (*CreateReply, error)
+	Verify(ctx context.Context, in *VerifyRequest, opts ...grpc.CallOption) (*VerifyReply, error)
 	Save(ctx context.Context, in *SaveRequest, opts ...grpc.CallOption) (*SaveReply, error)
 	Delete(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*DeleteReply, error)
 	Has(ctx context.Context, in *HasRequest, opts ...grpc.CallOption) (*HasReply, error)
@@ -2921,6 +3043,15 @@ func (c *aPIClient) Create(ctx context.Context, in *CreateRequest, opts ...grpc.
 	return out, nil
 }
 
+func (c *aPIClient) Verify(ctx context.Context, in *VerifyRequest, opts ...grpc.CallOption) (*VerifyReply, error) {
+	out := new(VerifyReply)
+	err := c.cc.Invoke(ctx, "/threads.pb.API/Verify", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *aPIClient) Save(ctx context.Context, in *SaveRequest, opts ...grpc.CallOption) (*SaveReply, error) {
 	out := new(SaveReply)
 	err := c.cc.Invoke(ctx, "/threads.pb.API/Save", in, out, opts...)
@@ -3075,6 +3206,7 @@ type APIServer interface {
 	GetCollectionIndexes(context.Context, *GetCollectionIndexesRequest) (*GetCollectionIndexesReply, error)
 	ListCollections(context.Context, *ListCollectionsRequest) (*ListCollectionsReply, error)
 	Create(context.Context, *CreateRequest) (*CreateReply, error)
+	Verify(context.Context, *VerifyRequest) (*VerifyReply, error)
 	Save(context.Context, *SaveRequest) (*SaveReply, error)
 	Delete(context.Context, *DeleteRequest) (*DeleteReply, error)
 	Has(context.Context, *HasRequest) (*HasReply, error)
@@ -3128,6 +3260,9 @@ func (*UnimplementedAPIServer) ListCollections(ctx context.Context, req *ListCol
 func (*UnimplementedAPIServer) Create(ctx context.Context, req *CreateRequest) (*CreateReply, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method Create not implemented")
 }
+func (*UnimplementedAPIServer) Verify(ctx context.Context, req *VerifyRequest) (*VerifyReply, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method Verify not implemented")
+}
 func (*UnimplementedAPIServer) Save(ctx context.Context, req *SaveRequest) (*SaveReply, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method Save not implemented")
 }
@@ -3399,6 +3534,24 @@ func _API_Create_Handler(srv interface{}, ctx context.Context, dec func(interfac
 	return interceptor(ctx, in, info, handler)
 }
 
+func _API_Verify_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(VerifyRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(APIServer).Verify(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/threads.pb.API/Verify",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(APIServer).Verify(ctx, req.(*VerifyRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _API_Save_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(SaveRequest)
 	if err := dec(in); err != nil {
@@ -3614,6 +3767,10 @@ var _API_serviceDesc = grpc.ServiceDesc{
 			MethodName: "Create",
 			Handler:    _API_Create_Handler,
 		},
+		{
+			MethodName: "Verify",
+			Handler:    _API_Verify_Handler,
+		},
 		{
 			MethodName: "Save",
 			Handler:    _API_Save_Handler,
diff --git a/api/pb/threads.proto b/api/pb/threads.proto
index 6e54462..e763624 100644
--- a/api/pb/threads.proto
+++ b/api/pb/threads.proto
@@ -137,6 +137,14 @@ message CreateReply {
     repeated string instanceIDs = 1;
 }
 
+message VerifyRequest {
+    bytes dbID = 1;
+    string collectionName = 2;
+    repeated bytes instances = 3;
+}
+
+message VerifyReply {}
+
 message SaveRequest {
     bytes dbID = 1;
     string collectionName = 2;
@@ -209,6 +217,7 @@ message WriteTransactionRequest {
     oneof option {
         StartTransactionRequest startTransactionRequest = 1;
         CreateRequest createRequest = 2;
+        VerifyRequest verifyRequest = 8;
         SaveRequest saveRequest = 3;
         DeleteRequest deleteRequest = 4;
         HasRequest hasRequest = 5;
@@ -220,6 +229,7 @@ message WriteTransactionRequest {
 message WriteTransactionReply {
     oneof option {
         CreateReply createReply = 1;
+        VerifyReply verifyReply = 7;
         SaveReply saveReply = 2;
         DeleteReply deleteReply = 3;
         HasReply hasReply = 4;
@@ -274,6 +284,7 @@ service API {
     }
     rpc ListCollections(ListCollectionsRequest) returns (ListCollectionsReply) {}
     rpc Create(CreateRequest) returns (CreateReply) {}
+    rpc Verify(VerifyRequest) returns (VerifyReply) {}
     rpc Save(SaveRequest) returns (SaveReply) {}
     rpc Delete(DeleteRequest) returns (DeleteReply) {}
     rpc Has(HasRequest) returns (HasReply) {}
diff --git a/api/service.go b/api/service.go
index 37013b4..2605f31 100644
--- a/api/service.go
+++ b/api/service.go
@@ -482,6 +482,22 @@ func (s *Service) Create(ctx context.Context, req *pb.CreateRequest) (*pb.Create
 	return s.processCreateRequest(req, token, collection.CreateMany)
 }
 
+func (s *Service) Verify(ctx context.Context, req *pb.VerifyRequest) (*pb.VerifyReply, error) {
+	id, err := thread.Cast(req.DbID)
+	if err != nil {
+		return nil, status.Error(codes.InvalidArgument, err.Error())
+	}
+	token, err := thread.NewTokenFromMD(ctx)
+	if err != nil {
+		return nil, err
+	}
+	collection, err := s.getCollection(ctx, req.CollectionName, id, token)
+	if err != nil {
+		return nil, err
+	}
+	return s.processVerifyRequest(req, token, collection.VerifyMany)
+}
+
 func (s *Service) Save(ctx context.Context, req *pb.SaveRequest) (*pb.SaveReply, error) {
 	id, err := thread.Cast(req.DbID)
 	if err != nil {
@@ -728,6 +744,17 @@ func (s *Service) WriteTransaction(stream pb.API_WriteTransactionServer) error {
 				if err := stream.Send(&pb.WriteTransactionReply{Option: option}); err != nil {
 					return err
 				}
+			case *pb.WriteTransactionRequest_VerifyRequest:
+				innerReply, err := s.processVerifyRequest(x.VerifyRequest, token, func(ids [][]byte, _ ...db.TxnOption) error {
+					return txn.Verify(ids...)
+				})
+				if err != nil {
+					return err
+				}
+				option := &pb.WriteTransactionReply_VerifyReply{VerifyReply: innerReply}
+				if err := stream.Send(&pb.WriteTransactionReply{Option: option}); err != nil {
+					return err
+				}
 			case *pb.WriteTransactionRequest_SaveRequest:
 				innerReply, err := s.processSaveRequest(x.SaveRequest, token, func(ids [][]byte, _ ...db.TxnOption) error {
 					return txn.Save(ids...)
@@ -867,6 +894,13 @@ func (s *Service) processCreateRequest(req *pb.CreateRequest, token thread.Token
 	return reply, nil
 }
 
+func (s *Service) processVerifyRequest(req *pb.VerifyRequest, token thread.Token, verifyFunc func([][]byte, ...db.TxnOption) error) (*pb.VerifyReply, error) {
+	if err := verifyFunc(req.Instances, db.WithTxnToken(token)); err != nil {
+		return nil, err
+	}
+	return &pb.VerifyReply{}, nil
+}
+
 func (s *Service) processSaveRequest(req *pb.SaveRequest, token thread.Token, saveFunc func([][]byte, ...db.TxnOption) error) (*pb.SaveReply, error) {
 	if err := saveFunc(req.Instances, db.WithTxnToken(token)); err != nil {
 		return nil, err
diff --git a/cbor/coding.go b/cbor/coding.go
index b7ae3eb..68a9794 100644
--- a/cbor/coding.go
+++ b/cbor/coding.go
@@ -3,7 +3,7 @@ package cbor
 import (
 	blocks "github.com/ipfs/go-block-format"
 	"github.com/ipfs/go-ipld-cbor"
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	mh "github.com/multiformats/go-multihash"
 	"github.com/textileio/go-threads/crypto"
 )
diff --git a/cbor/event.go b/cbor/event.go
index 8cb184e..5e1c90f 100644
--- a/cbor/event.go
+++ b/cbor/event.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/ipfs/go-cid"
 	cbornode "github.com/ipfs/go-ipld-cbor"
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	mh "github.com/multiformats/go-multihash"
 	"github.com/textileio/go-threads/core/net"
 	"github.com/textileio/go-threads/crypto"
diff --git a/cbor/record.go b/cbor/record.go
index 0133d32..df35106 100644
--- a/cbor/record.go
+++ b/cbor/record.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/ipfs/go-cid"
 	cbornode "github.com/ipfs/go-ipld-cbor"
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	ic "github.com/libp2p/go-libp2p-core/crypto"
 	mh "github.com/multiformats/go-multihash"
 	"github.com/textileio/go-threads/core/net"
diff --git a/core/app/app.go b/core/app/app.go
index 80b2c64..71bdc73 100644
--- a/core/app/app.go
+++ b/core/app/app.go
@@ -5,7 +5,7 @@ import (
 	"errors"
 	"time"
 
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	logging "github.com/ipfs/go-log"
 	"github.com/textileio/go-threads/broadcast"
 	"github.com/textileio/go-threads/core/net"
diff --git a/core/db/db.go b/core/db/db.go
index 1dfbf42..9d4ca3c 100644
--- a/core/db/db.go
+++ b/core/db/db.go
@@ -4,7 +4,7 @@ import (
 	"crypto/rand"
 	"strings"
 
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	ulid "github.com/oklog/ulid/v2"
 	ds "github.com/textileio/go-datastore"
 )
diff --git a/core/net/net.go b/core/net/net.go
index ae1363d..aaf4c86 100644
--- a/core/net/net.go
+++ b/core/net/net.go
@@ -6,7 +6,7 @@ import (
 	"io"
 
 	"github.com/ipfs/go-cid"
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	"github.com/libp2p/go-libp2p-core/host"
 	"github.com/libp2p/go-libp2p-core/peer"
 	ma "github.com/multiformats/go-multiaddr"
diff --git a/db/collection.go b/db/collection.go
index 48538f7..96d3224 100644
--- a/db/collection.go
+++ b/db/collection.go
@@ -11,6 +11,7 @@ import (
 	"github.com/alecthomas/jsonschema"
 	"github.com/dop251/goja"
 	jsonpatch "github.com/evanphx/json-patch"
+	"github.com/ipfs/go-ipld-format"
 	ds "github.com/textileio/go-datastore"
 	"github.com/textileio/go-threads/core/app"
 	core "github.com/textileio/go-threads/core/db"
@@ -219,6 +220,20 @@ func (c *Collection) SaveMany(vs [][]byte, opts ...TxnOption) error {
 	}, opts...)
 }
 
+// Verify verifies changes of an instance in the collection.
+func (c *Collection) Verify(v []byte, opts ...TxnOption) error {
+	return c.WriteTxn(func(txn *Txn) error {
+		return txn.Verify(v)
+	}, opts...)
+}
+
+// VerifyMany verifies changes of multiple instances in the collection.
+func (c *Collection) VerifyMany(vs [][]byte, opts ...TxnOption) error {
+	return c.WriteTxn(func(txn *Txn) error {
+		return txn.Verify(vs...)
+	}, opts...)
+}
+
 // Has returns true if ID exists in the collection, false
 // otherwise.
 func (c *Collection) Has(id core.InstanceID, opts ...TxnOption) (exists bool, err error) {
@@ -404,42 +419,86 @@ func (t *Txn) Create(new ...[]byte) ([]core.InstanceID, error) {
 	return results, nil
 }
 
+// Verify verifies updated instances but does not save them.
+func (t *Txn) Verify(updated ...[]byte) error {
+	identity, err := t.token.PubKey()
+	if err != nil {
+		return err
+	}
+	actions, err := t.createSaveActions(identity, updated...)
+	if err != nil {
+		return err
+	}
+
+	events, _, err := t.createEvents(actions)
+	if err != nil {
+		return err
+	}
+	if len(events) == 0 {
+		return nil
+	}
+	for _, e := range events {
+		if err := t.collection.validWrite(identity, e); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 // Save saves an instance changes to be committed when the current transaction commits.
 func (t *Txn) Save(updated ...[]byte) error {
+	identity, err := t.token.PubKey()
+	if err != nil {
+		return err
+	}
+	actions, err := t.createSaveActions(identity, updated...)
+	if err != nil {
+		return err
+	}
+	t.actions = append(t.actions, actions...)
+	return nil
+}
+
+func (t *Txn) createSaveActions(identity thread.PubKey, updated ...[]byte) ([]core.Action, error) {
+	var actions []core.Action
 	for i := range updated {
 		if t.readonly {
-			return ErrReadonlyTx
+			return nil, ErrReadonlyTx
 		}
 
-		item := make([]byte, len(updated[i]))
-		copy(item, updated[i])
+		next := make([]byte, len(updated[i]))
+		copy(next, updated[i])
 
-		if err := t.collection.validInstance(item); err != nil {
-			return err
+		if err := t.collection.validInstance(next); err != nil {
+			return nil, err
 		}
 
-		id, err := getInstanceID(item)
+		id, err := getInstanceID(next)
 		if err != nil {
-			return err
+			return nil, err
 		}
 		key := baseKey.ChildString(t.collection.name).ChildString(id.String())
-		beforeBytes, err := t.collection.db.datastore.Get(key)
+		previous, err := t.collection.db.datastore.Get(key)
 		if err == ds.ErrNotFound {
-			return errCantSaveNonExistentInstance
+			return nil, errCantSaveNonExistentInstance
 		}
 		if err != nil {
-			return err
+			return nil, err
+		}
+		previous, err = t.collection.filterRead(identity, previous)
+		if err != nil {
+			return nil, err
 		}
 
-		t.actions = append(t.actions, core.Action{
+		actions = append(actions, core.Action{
 			Type:           core.Save,
 			InstanceID:     id,
 			CollectionName: t.collection.name,
-			Previous:       beforeBytes,
-			Current:        item,
+			Previous:       previous,
+			Current:        next,
 		})
 	}
-	return nil
+	return actions, nil
 }
 
 // Delete deletes instances by ID when the current transaction commits.
@@ -536,19 +595,13 @@ func (t *Txn) FindByID(id core.InstanceID) ([]byte, error) {
 // to the collection. This is a syncrhonous call so changes can
 // be assumed to be applied on function return.
 func (t *Txn) Commit() error {
-	if t.discarded || t.committed {
-		return errAlreadyDiscardedCommitedTxn
-	}
-	events, node, err := t.collection.db.eventcodec.Create(t.actions)
+	events, node, err := t.createEvents(t.actions)
 	if err != nil {
 		return err
 	}
-	if len(events) == 0 && node == nil {
+	if node == nil {
 		return nil
 	}
-	if len(events) == 0 || node == nil {
-		return fmt.Errorf("created events and node must both be nil or not-nil")
-	}
 
 	ctx, cancel := context.WithTimeout(context.Background(), createNetRecordTimeout)
 	defer cancel()
@@ -625,6 +678,23 @@ func setNewInstanceID(t []byte) (core.InstanceID, []byte) {
 	return newID, patchedValue
 }
 
+func (t *Txn) createEvents(actions []core.Action) (events []core.Event, node format.Node, err error) {
+	if t.discarded || t.committed {
+		return nil, nil, errAlreadyDiscardedCommitedTxn
+	}
+	events, node, err = t.collection.db.eventcodec.Create(actions)
+	if err != nil {
+		return
+	}
+	if len(events) == 0 && node == nil {
+		return nil, nil, nil
+	}
+	if len(events) == 0 || node == nil {
+		return nil, nil, fmt.Errorf("created events and node must both be nil or not-nil")
+	}
+	return events, node, nil
+}
+
 func compileJSFunc(v []byte, name string, args ...string) ([]byte, error) {
 	if len(v) == 0 {
 		return nil, nil
diff --git a/db/db.go b/db/db.go
index 18a9a2f..8121e08 100644
--- a/db/db.go
+++ b/db/db.go
@@ -13,7 +13,7 @@ import (
 
 	"github.com/alecthomas/jsonschema"
 	"github.com/dop251/goja"
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	logging "github.com/ipfs/go-log"
 	ma "github.com/multiformats/go-multiaddr"
 	ds "github.com/textileio/go-datastore"
@@ -552,7 +552,9 @@ func (d *DB) ValidateNetRecordBody(_ context.Context, body format.Node, identity
 		if !ok {
 			return ErrCollectionNotFound
 		}
-		return c.validWrite(identity, e)
+		if err := c.validWrite(identity, e); err != nil {
+			return err
+		}
 	}
 	return nil
 }
diff --git a/db/listeners.go b/db/listeners.go
index b91f8c9..cba1b15 100644
--- a/db/listeners.go
+++ b/db/listeners.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"sync"
 
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	"github.com/textileio/go-threads/core/app"
 	core "github.com/textileio/go-threads/core/db"
 	"github.com/textileio/go-threads/core/thread"
diff --git a/jsonpatcher/jsonpatcher.go b/jsonpatcher/jsonpatcher.go
index c020531..eed2bf9 100644
--- a/jsonpatcher/jsonpatcher.go
+++ b/jsonpatcher/jsonpatcher.go
@@ -11,7 +11,7 @@ import (
 
 	jsonpatch "github.com/evanphx/json-patch"
 	cbornode "github.com/ipfs/go-ipld-cbor"
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	logging "github.com/ipfs/go-log"
 	"github.com/multiformats/go-multihash"
 	ds "github.com/textileio/go-datastore"
diff --git a/net/api/client/client.go b/net/api/client/client.go
index af2678e..fdc6b65 100644
--- a/net/api/client/client.go
+++ b/net/api/client/client.go
@@ -7,7 +7,7 @@ import (
 	"log"
 
 	"github.com/ipfs/go-cid"
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	ic "github.com/libp2p/go-libp2p-core/crypto"
 	"github.com/libp2p/go-libp2p-core/peer"
 	ma "github.com/multiformats/go-multiaddr"
diff --git a/net/net.go b/net/net.go
index 6edcb59..42f8407 100644
--- a/net/net.go
+++ b/net/net.go
@@ -12,7 +12,7 @@ import (
 
 	"github.com/ipfs/go-cid"
 	bs "github.com/ipfs/go-ipfs-blockstore"
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	logging "github.com/ipfs/go-log"
 	"github.com/libp2p/go-libp2p-core/crypto"
 	"github.com/libp2p/go-libp2p-core/host"


Test patch:
diff --git a/api/client/client_test.go b/api/client/client_test.go
index ddbc850..8bf7e88 100644
--- a/api/client/client_test.go
+++ b/api/client/client_test.go
@@ -357,6 +357,53 @@ func TestClient_Create(t *testing.T) {
 	})
 }
 
+func TestClient_Verify(t *testing.T) {
+	t.Parallel()
+	client, done := setup(t)
+	defer done()
+
+	t.Run("test collection verify", func(t *testing.T) {
+		id := thread.NewIDV1(thread.Raw, 32)
+		err := client.NewDB(context.Background(), id)
+		checkErr(t, err)
+		err = client.NewCollection(context.Background(), id, db.CollectionConfig{
+			Name:   collectionName,
+			Schema: util.SchemaFromSchemaString(schema),
+			WriteValidator: `
+				var type = event.patch.type
+				var patch = event.patch.json_patch
+				switch (type) {
+				  case "delete":
+				    return false
+				  default:
+				    if (patch.age > 50) {
+				      return false
+				    }
+				    return true
+				}
+			`,
+		})
+		checkErr(t, err)
+
+		person := createPerson()
+
+		ids, err := client.Create(context.Background(), id, collectionName, Instances{person})
+		checkErr(t, err)
+
+		person.ID = ids[0]
+		person.Age = 51
+		err = client.Verify(context.Background(), id, collectionName, Instances{person})
+		if err == nil {
+			t.Fatalf("failed to verify instance")
+		}
+		person.Age = 50
+		err = client.Verify(context.Background(), id, collectionName, Instances{person})
+		if err != nil {
+			t.Fatalf("failed to verify instance")
+		}
+	})
+}
+
 func TestClient_Save(t *testing.T) {
 	t.Parallel()
 	client, done := setup(t)
@@ -687,6 +734,10 @@ func TestClient_WriteTransaction(t *testing.T) {
 		}
 
 		existingPerson.Age = 99
+		err = txn.Verify(existingPerson)
+		if err != nil {
+			t.Fatalf("failed to verify in write txn: %v", err)
+		}
 		err = txn.Save(existingPerson)
 		if err != nil {
 			t.Fatalf("failed to save in write txn: %v", err)
diff --git a/db/collection_test.go b/db/collection_test.go
index c4cedd0..ecd5418 100644
--- a/db/collection_test.go
+++ b/db/collection_test.go
@@ -107,18 +107,39 @@ func TestNewCollection(t *testing.T) {
 		db, clean := createTestDB(t)
 		defer clean()
 		c, err := db.NewCollection(CollectionConfig{
-			Name:           "Dog",
-			Schema:         util.SchemaFromInstance(&Dog{}, false),
-			WriteValidator: `return true`,
+			Name:   "Dog",
+			Schema: util.SchemaFromInstance(&Dog{}, false),
+			WriteValidator: `
+				var type = event.patch.type
+				var patch = event.patch.json_patch
+				switch (type) {
+				  case "delete":
+				    return false
+				  default:
+				    if (patch.Name !== "Fido" && patch.Name != "Clyde") {
+				      return false
+				    }
+				    return true
+				}
+			`,
 		})
 		checkErr(t, err)
 		dog := Dog{Name: "Fido", Comments: []Comment{}}
 		id, err := c.Create(util.JSONFromInstance(dog))
 		checkErr(t, err)
 		dog.ID = id
+		dog.Name = "Bob"
+		err = c.Save(util.JSONFromInstance(dog))
+		if err == nil {
+			t.Fatal("save should have been invalid")
+		}
 		dog.Name = "Clyde"
 		err = c.Save(util.JSONFromInstance(dog))
 		checkErr(t, err)
+		err = c.Delete(dog.ID)
+		if err == nil {
+			t.Fatal("delete should have been invalid")
+		}
 	})
 	t.Run("WithReadFilter", func(t *testing.T) {
 		t.Parallel()
@@ -811,6 +832,82 @@ func TestGetInstance(t *testing.T) {
 	})
 }
 
+func TestVerifyInstance(t *testing.T) {
+	t.Parallel()
+	t.Run("WithoutWriteValidator", func(t *testing.T) {
+		t.Parallel()
+		db, clean := createTestDB(t)
+		defer clean()
+		c, err := db.NewCollection(CollectionConfig{
+			Name:   "Person",
+			Schema: util.SchemaFromInstance(&Person{}, false),
+		})
+		checkErr(t, err)
+
+		newPerson := util.JSONFromInstance(Person{Name: "Alice", Age: 42})
+		var res []core.InstanceID
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			res, err = txn.Create(newPerson)
+			return
+		})
+		checkErr(t, err)
+
+		newPerson = util.JSONFromInstance(Person{ID: res[0], Name: "Alice", Age: 43})
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			err = txn.Verify(newPerson)
+			return
+		})
+		checkErr(t, err)
+	})
+	t.Run("WithWriteValidator", func(t *testing.T) {
+		t.Parallel()
+		db, clean := createTestDB(t)
+		defer clean()
+		c, err := db.NewCollection(CollectionConfig{
+			Name:   "Person",
+			Schema: util.SchemaFromInstance(&Person{}, false),
+			WriteValidator: `
+				var type = event.patch.type
+				var patch = event.patch.json_patch
+				switch (type) {
+				  case "delete":
+				    return false
+				  default:
+				    if (patch.Age > 50) {
+				      return false
+				    }
+				    return true
+				}
+			`,
+		})
+		checkErr(t, err)
+
+		newPerson := util.JSONFromInstance(Person{Name: "Alice", Age: 42})
+		var res []core.InstanceID
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			res, err = txn.Create(newPerson)
+			return
+		})
+		checkErr(t, err)
+
+		newPerson = util.JSONFromInstance(Person{ID: res[0], Name: "Alice", Age: 51})
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			err = txn.Verify(newPerson)
+			return
+		})
+		if err == nil {
+			t.Fatal("write should have been invalid")
+		}
+
+		newPerson = util.JSONFromInstance(Person{ID: res[0], Name: "Alice", Age: 43})
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			err = txn.Verify(newPerson)
+			return
+		})
+		checkErr(t, err)
+	})
+}
+
 func TestSaveInstance(t *testing.T) {
 	t.Parallel()
 	t.Run("Simple", func(t *testing.T) {
diff --git a/db/db_test.go b/db/db_test.go
index 53c4b56..294e519 100644
--- a/db/db_test.go
+++ b/db/db_test.go
@@ -11,7 +11,7 @@ import (
 	"testing"
 	"time"
 
-	format "github.com/ipfs/go-ipld-format"
+	"github.com/ipfs/go-ipld-format"
 	"github.com/multiformats/go-multiaddr"
 	ds "github.com/textileio/go-datastore"
 	"github.com/textileio/go-threads/common"