diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index d12a850..82c913b 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,9 +4,9 @@
     
   
   
-    
+    
       
-      
+      
     
@@ -139,8 +139,6 @@
-    
-    
     
     
     
@@ -164,7 +162,9 @@
     
     
     
-    
+    
+    
+    
   
   
     true
diff --git a/pkg/grpc/auth.go b/pkg/grpc/auth.go
index 87807d8..1f00a7d 100644
--- a/pkg/grpc/auth.go
+++ b/pkg/grpc/auth.go
@@ -3,6 +3,7 @@ package grpc
 import (
 	"context"
 	"fmt"
+	jsoniter "github.com/json-iterator/go"
 
 	"git.solsynth.dev/hydrogen/passport/pkg/grpc/proto"
 	"git.solsynth.dev/hydrogen/passport/pkg/services"
@@ -10,16 +11,18 @@ import (
 )
 
 func (v *Server) Authenticate(_ context.Context, in *proto.AuthRequest) (*proto.AuthReply, error) {
-	user, atk, rtk, err := services.Authenticate(in.GetAccessToken(), in.GetRefreshToken(), 0)
+	user, perms, atk, rtk, err := services.Authenticate(in.GetAccessToken(), in.GetRefreshToken(), 0)
 	if err != nil {
 		return &proto.AuthReply{
 			IsValid: false,
 		}, nil
 	} else {
+		rawPerms, _ := jsoniter.Marshal(perms)
 		return &proto.AuthReply{
 			IsValid:      true,
 			AccessToken:  &atk,
 			RefreshToken: &rtk,
+			Permissions:  rawPerms,
 			Userinfo: &proto.Userinfo{
 				Id:          uint64(user.ID),
 				Name:        user.Name,
diff --git a/pkg/grpc/proto/auth.pb.go b/pkg/grpc/proto/auth.pb.go
index 1870972..cc9144a 100644
--- a/pkg/grpc/proto/auth.pb.go
+++ b/pkg/grpc/proto/auth.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.33.0
-// 	protoc        v4.25.3
+// 	protoc        v5.26.1
 // source: auth.proto
 
 package proto
@@ -179,6 +179,7 @@ type AuthReply struct {
 	AccessToken  *string   `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3,oneof" json:"access_token,omitempty"`
 	RefreshToken *string   `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3,oneof" json:"refresh_token,omitempty"`
 	Userinfo     *Userinfo `protobuf:"bytes,4,opt,name=userinfo,proto3,oneof" json:"userinfo,omitempty"`
+	Permissions  []byte    `protobuf:"bytes,5,opt,name=permissions,proto3,oneof" json:"permissions,omitempty"`
 }
 
 func (x *AuthReply) Reset() {
@@ -241,6 +242,13 @@ func (x *AuthReply) GetUserinfo() *Userinfo {
 	return nil
 }
 
+func (x *AuthReply) GetPermissions() []byte {
+	if x != nil {
+		return x.Permissions
+	}
+	return nil
+}
+
 var File_auth_proto protoreflect.FileDescriptor
 
 var file_auth_proto_rawDesc = []byte{
@@ -264,7 +272,7 @@ var file_auth_proto_rawDesc = []byte{
 	0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00,
 	0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x88, 0x01,
 	0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f,
-	0x6b, 0x65, 0x6e, 0x22, 0xda, 0x01, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x70, 0x6c,
+	0x6b, 0x65, 0x6e, 0x22, 0x91, 0x02, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x70, 0x6c,
 	0x79, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20,
 	0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x12, 0x26, 0x0a, 0x0c,
 	0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01,
@@ -275,15 +283,18 @@ var file_auth_proto_rawDesc = []byte{
 	0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b,
 	0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66,
 	0x6f, 0x48, 0x02, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x88, 0x01, 0x01,
-	0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65,
-	0x6e, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f,
-	0x6b, 0x65, 0x6e, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f,
-	0x32, 0x3e, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x36, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68,
-	0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00,
-	0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x33,
+	0x12, 0x25, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18,
+	0x05, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x03, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73,
+	0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x61, 0x63, 0x63, 0x65,
+	0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x66,
+	0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x75,
+	0x73, 0x65, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x70, 0x65, 0x72, 0x6d,
+	0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x32, 0x3e, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12,
+	0x36, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12,
+	0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x75, 0x74, 0x68,
+	0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/pkg/grpc/proto/auth.proto b/pkg/grpc/proto/auth.proto
index 456dd2b..b0b8365 100644
--- a/pkg/grpc/proto/auth.proto
+++ b/pkg/grpc/proto/auth.proto
@@ -28,4 +28,5 @@ message AuthReply {
   optional string access_token = 2;
   optional string refresh_token = 3;
   optional Userinfo userinfo = 4;
+  optional bytes permissions = 5;
 }
\ No newline at end of file
diff --git a/pkg/grpc/proto/auth_grpc.pb.go b/pkg/grpc/proto/auth_grpc.pb.go
index 3df2f5d..b794591 100644
--- a/pkg/grpc/proto/auth_grpc.pb.go
+++ b/pkg/grpc/proto/auth_grpc.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
 // versions:
 // - protoc-gen-go-grpc v1.3.0
-// - protoc             v4.25.3
+// - protoc             v5.26.1
 // source: auth.proto
 
 package proto
diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go
index 12d2bff..d1f506d 100644
--- a/pkg/models/accounts.go
+++ b/pkg/models/accounts.go
@@ -6,18 +6,19 @@ import (
 
 	"github.com/samber/lo"
 	"github.com/spf13/viper"
+	"gorm.io/datatypes"
 )
 
 type Account struct {
 	BaseModel
 
-	Name        string     `json:"name" gorm:"uniqueIndex"`
-	Nick        string     `json:"nick"`
-	Description string     `json:"description"`
-	Avatar      string     `json:"avatar"`
-	Banner      string     `json:"banner"`
-	ConfirmedAt *time.Time `json:"confirmed_at"`
-	PowerLevel  int        `json:"power_level"`
+	Name        string            `json:"name" gorm:"uniqueIndex"`
+	Nick        string            `json:"nick"`
+	Description string            `json:"description"`
+	Avatar      string            `json:"avatar"`
+	Banner      string            `json:"banner"`
+	ConfirmedAt *time.Time        `json:"confirmed_at"`
+	PermNodes   datatypes.JSONMap `json:"perm_nodes"`
 
 	Profile         AccountProfile   `json:"profile"`
 	PersonalPage    AccountPage      `json:"personal_page"`
diff --git a/pkg/server/auth_middleware.go b/pkg/server/auth_middleware.go
index adb133b..0576110 100644
--- a/pkg/server/auth_middleware.go
+++ b/pkg/server/auth_middleware.go
@@ -42,10 +42,11 @@ func authFunc(c *fiber.Ctx, overrides ...string) error {
 	}
 
 	rtk := c.Cookies(services.CookieRefreshKey)
-	if user, atk, rtk, err := services.Authenticate(token, rtk, 0); err == nil {
+	if user, perms, atk, rtk, err := services.Authenticate(token, rtk, 0); err == nil {
 		if atk != token {
 			services.SetJwtCookieSet(c, atk, rtk)
 		}
+		c.Locals("permissions", perms)
 		c.Locals("principal", user)
 		return nil
 	} else {
diff --git a/pkg/server/ws.go b/pkg/server/ws.go
index ae1bf31..0c18af9 100644
--- a/pkg/server/ws.go
+++ b/pkg/server/ws.go
@@ -42,6 +42,7 @@ func listenWebsocket(c *websocket.Conn) {
 			var req struct {
 				RequestID string `json:"request_id"`
 				KeypairID string `json:"keypair_id"`
+				Algorithm string `json:"algorithm"`
 				OwnerID   uint   `json:"owner_id"`
 				Deadline  int64  `json:"deadline"`
 			}
@@ -49,11 +50,12 @@ func listenWebsocket(c *websocket.Conn) {
 			if len(req.RequestID) <= 0 || len(req.KeypairID) <= 0 || req.OwnerID <= 0 {
 				message = lo.ToPtr(models.UnifiedCommandFromError(fmt.Errorf("invalid request")))
 			}
-			services.KexRequest(c, req.RequestID, req.KeypairID, req.OwnerID, req.Deadline)
+			services.KexRequest(c, req.RequestID, req.KeypairID, req.Algorithm, req.OwnerID, req.Deadline)
 		case "kex.provide":
 			var req struct {
 				RequestID string `json:"request_id"`
 				KeypairID string `json:"keypair_id"`
+				Algorithm string `json:"algorithm"`
 				PublicKey []byte `json:"public_key"`
 			}
 			_ = jsoniter.Unmarshal(payload, &req)
diff --git a/pkg/services/accounts.go b/pkg/services/accounts.go
index 63801aa..7739a8a 100644
--- a/pkg/services/accounts.go
+++ b/pkg/services/accounts.go
@@ -2,6 +2,8 @@ package services
 
 import (
 	"fmt"
+	"github.com/spf13/viper"
+	"gorm.io/datatypes"
 	"time"
 
 	"git.solsynth.dev/hydrogen/passport/pkg/database"
@@ -66,7 +68,7 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) {
 				VerifiedAt: nil,
 			},
 		},
-		PowerLevel:  0,
+		PermNodes:   datatypes.JSONMap(viper.GetStringMap("permissions.default")),
 		ConfirmedAt: nil,
 	}
 
@@ -98,7 +100,14 @@ func ConfirmAccount(code string) error {
 
 	return database.C.Transaction(func(tx *gorm.DB) error {
 		user.ConfirmedAt = lo.ToPtr(time.Now())
-		user.PowerLevel += 5
+
+		for k, v := range viper.GetStringMap("permissions.verified") {
+			if val, ok := user.PermNodes[k]; !ok {
+				user.PermNodes[k] = v
+			} else if !HasPermNode(val, v) {
+				user.PermNodes[k] = v
+			}
+		}
 
 		if err := database.C.Delete(&token).Error; err != nil {
 			return err
diff --git a/pkg/services/auth.go b/pkg/services/auth.go
index 9141601..1cbf35a 100644
--- a/pkg/services/auth.go
+++ b/pkg/services/auth.go
@@ -14,7 +14,7 @@ import (
 
 const authContextBucket = "AuthContext"
 
-func Authenticate(access, refresh string, depth int) (user models.Account, newAccess, newRefresh string, err error) {
+func Authenticate(access, refresh string, depth int) (user models.Account, perms map[string]any, newAccess, newRefresh string, err error) {
 	var claims PayloadClaims
 	claims, err = DecodeJwt(access)
 	if err != nil {
@@ -37,6 +37,7 @@ func Authenticate(access, refresh string, depth int) (user models.Account, newAc
 	ctx, lookupErr := GetAuthContext(claims.ID)
 	if lookupErr == nil {
 		log.Debug().Str("jti", claims.ID).Msg("Hit auth context cache once!")
+		perms = FilterPermNodes(ctx.Account.PermNodes, ctx.Ticket.Claims)
 		user = ctx.Account
 		return
 	}
@@ -44,6 +45,7 @@ func Authenticate(access, refresh string, depth int) (user models.Account, newAc
 	ctx, err = GrantAuthContext(claims.ID)
 	if err == nil {
 		log.Debug().Str("jti", claims.ID).Err(lookupErr).Msg("Missed auth context cache once!")
+		perms = FilterPermNodes(ctx.Account.PermNodes, ctx.Ticket.Claims)
 		user = ctx.Account
 		return
 	}
@@ -97,7 +99,7 @@ func GrantAuthContext(jti string) (models.AuthContext, error) {
 		return ctx, fmt.Errorf("invalid account: %v", err)
 	}
 
-	// Every context should expires in some while
+	// Every context should expire in some while
 	// Once user update their account info, this will have delay to update
 	ctx = models.AuthContext{
 		Ticket:    ticket,
diff --git a/pkg/services/e2ee.go b/pkg/services/e2ee.go
index 19b4d07..6197c02 100644
--- a/pkg/services/e2ee.go
+++ b/pkg/services/e2ee.go
@@ -15,7 +15,7 @@ type kexRequest struct {
 
 var kexRequests = make(map[string]map[string]kexRequest)
 
-func KexRequest(conn *websocket.Conn, requestId, keypairId string, ownerId uint, deadline int64) {
+func KexRequest(conn *websocket.Conn, requestId, keypairId, algorithm string, ownerId uint, deadline int64) {
 	if kexRequests[keypairId] == nil {
 		kexRequests[keypairId] = make(map[string]kexRequest)
 	}
@@ -38,6 +38,7 @@ func KexRequest(conn *websocket.Conn, requestId, keypairId string, ownerId uint,
 			Payload: fiber.Map{
 				"request_id": requestId,
 				"keypair_id": keypairId,
+				"algorithm":  algorithm,
 				"owner_id":   ownerId,
 				"deadline":   deadline,
 			},
diff --git a/pkg/services/perms.go b/pkg/services/perms.go
new file mode 100644
index 0000000..9a6914d
--- /dev/null
+++ b/pkg/services/perms.go
@@ -0,0 +1,55 @@
+package services
+
+import (
+	"reflect"
+	"regexp"
+	"strings"
+)
+
+func HasPermNode(held any, required any) bool {
+	heldValue := reflect.ValueOf(held)
+	requiredValue := reflect.ValueOf(required)
+
+	switch heldValue.Kind() {
+	case reflect.Int, reflect.Float64:
+		if heldValue.Float() >= requiredValue.Float() {
+			return true
+		}
+	case reflect.String:
+		if heldValue.String() == requiredValue.String() {
+			return true
+		}
+	case reflect.Slice, reflect.Array:
+		for i := 0; i < heldValue.Len(); i++ {
+			if reflect.DeepEqual(heldValue.Index(i).Interface(), required) {
+				return true
+			}
+		}
+	default:
+		if reflect.DeepEqual(held, required) {
+			return true
+		}
+	}
+
+	return false
+}
+
+func FilterPermNodes(tree map[string]any, claims []string) map[string]any {
+	filteredTree := make(map[string]any)
+
+	match := func(claim, permission string) bool {
+		regex := strings.Replace(permission, "*", ".*", -1)
+		match, _ := regexp.MatchString("^"+regex+"$", claim)
+		return match
+	}
+
+	for _, claim := range claims {
+		for key, value := range tree {
+			if match(claim, key) {
+				filteredTree[key] = value
+			}
+		}
+	}
+
+	return filteredTree
+}
diff --git a/settings.toml b/settings.toml
index 4430040..2b2bcf0 100644
--- a/settings.toml
+++ b/settings.toml
@@ -35,3 +35,9 @@ refresh_token_duration = 2592000
 dsn = "host=localhost dbname=hy_passport port=5432 sslmode=disable"
 prefix = "passport_"
 bolt = "uploads/bolt.db"
+
+[permissions.default]
+CreatePaperclipAttachments = 1048576
+
+[permissions.verified]
+CreatePaperclipAttachments = 26214400
\ No newline at end of file