diff --git a/.editorconfig b/.editorconfig
index fa54a6e..4aa2717 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,4 +1,4 @@
-root = true
+root = true
 
 [*]
 end_of_line = lf
@@ -11,3 +11,134 @@ indent_size = 4
 
 # IDE1006: Naming Styles
 dotnet_diagnostic.IDE1006.severity = none
+csharp_indent_labels = one_less_than_current
+csharp_using_directive_placement = outside_namespace:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_prefer_braces = true:silent
+csharp_style_namespace_declarations = block_scoped:silent
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_prefer_system_threading_lock = true:suggestion
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_prefer_static_local_function = true:suggestion
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_prefer_static_anonymous_function = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
+csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
+csharp_style_conditional_delegate_call = true:suggestion
+csharp_style_prefer_switch_expression = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_pattern_matching = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+csharp_style_var_elsewhere = false:silent
+
+[*.{cs,vb}]
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers = 
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers = 
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers = 
+
+# Naming styles
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix = 
+dotnet_naming_style.begins_with_i.word_separator = 
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix = 
+dotnet_naming_style.pascal_case.required_suffix = 
+dotnet_naming_style.pascal_case.word_separator = 
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix = 
+dotnet_naming_style.pascal_case.required_suffix = 
+dotnet_naming_style.pascal_case.word_separator = 
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_style_readonly_field = true:suggestion
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
+dotnet_style_allow_multiple_blank_lines_experimental = true:silent
+dotnet_code_quality_unused_parameters = all:suggestion
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+dotnet_style_qualification_for_property = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_method = false:silent
diff --git a/SharpChat.Misuzu/FlashiiAuthResult.cs b/SharpChat.Misuzu/FlashiiAuthResult.cs
new file mode 100644
index 0000000..8bcc823
--- /dev/null
+++ b/SharpChat.Misuzu/FlashiiAuthResult.cs
@@ -0,0 +1,31 @@
+using SharpChat.Auth;
+using System.Text.Json.Serialization;
+
+namespace SharpChat.Flashii {
+    public class FlashiiAuthResult : AuthResult {
+        public string UserId => UserIdRaw.ToString();
+        public string UserName => UserNameRaw ?? string.Empty;
+        public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw);
+
+        [JsonPropertyName("success")]
+        public bool Success { get; init; }
+
+        [JsonPropertyName("reason")]
+        public string? Reason { get; init; }
+
+        [JsonPropertyName("user_id")]
+        public long UserIdRaw { get; init; }
+
+        [JsonPropertyName("username")]
+        public string? UserNameRaw { get; init; }
+
+        [JsonPropertyName("colour_raw")]
+        public int UserColourRaw { get; init; }
+
+        [JsonPropertyName("hierarchy")]
+        public int UserRank { get; init; }
+
+        [JsonPropertyName("perms")]
+        public UserPermissions UserPermissions { get; init; }
+    }
+}
diff --git a/SharpChat.Misuzu/FlashiiBanInfo.cs b/SharpChat.Misuzu/FlashiiBanInfo.cs
new file mode 100644
index 0000000..ed6906c
--- /dev/null
+++ b/SharpChat.Misuzu/FlashiiBanInfo.cs
@@ -0,0 +1,13 @@
+using SharpChat.Bans;
+
+namespace SharpChat.Flashii {
+    public abstract class FlashiiBanInfo(
+        BanKind kind,
+        FlashiiRawBanInfo rawBanInfo
+    ) : BanInfo {
+        public BanKind Kind { get; } = kind;
+        public bool IsPermanent { get; } = rawBanInfo.IsPermanent;
+        public DateTimeOffset ExpiresAt { get; } = rawBanInfo.ExpiresAt;
+        public abstract override string ToString();
+    }
+}
diff --git a/SharpChat.Misuzu/FlashiiClient.cs b/SharpChat.Misuzu/FlashiiClient.cs
new file mode 100644
index 0000000..a749532
--- /dev/null
+++ b/SharpChat.Misuzu/FlashiiClient.cs
@@ -0,0 +1,240 @@
+using SharpChat.Auth;
+using SharpChat.Bans;
+using SharpChat.Configuration;
+using System.Net;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+
+namespace SharpChat.Flashii {
+    public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, BansClient {
+        private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat";
+        private readonly CachedValue<string> BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
+
+        private const string DEFAULT_SECRET_KEY = "woomy";
+        private readonly CachedValue<string> SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
+
+        private string CreateStringSignature(string str) {
+            return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
+        }
+
+        private string CreateBufferSignature(byte[] bytes) {
+            using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey!));
+            return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
+        }
+
+        private const string AUTH_VERIFY_URL = "{0}/verify";
+        private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}";
+
+        public async Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token) {
+            string remoteAddrStr = remoteAddr.ToString();
+
+            HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
+                Content = new FormUrlEncodedContent(new Dictionary<string, string> {
+                    { "method", scheme },
+                    { "token", token },
+                    { "ipaddr", remoteAddrStr },
+                }),
+                Headers = {
+                    { "X-SharpChat-Signature", CreateStringSignature(string.Format(AUTH_VERIFY_SIG, scheme, token, remoteAddrStr)) },
+                },
+            };
+
+            using HttpResponseMessage response = await httpClient.SendAsync(request);
+            response.EnsureSuccessStatusCode();
+
+            using Stream stream = await response.Content.ReadAsStreamAsync();
+            FlashiiAuthResult? authResult = await JsonSerializer.DeserializeAsync<FlashiiAuthResult>(stream);
+            if(authResult?.Success != true)
+                throw new AuthFailedException(authResult?.Reason ?? "none");
+
+            return authResult;
+        }
+
+        private const string AUTH_BUMP_USERS_ONLINE_URL = "{0}/bump";
+
+        public async Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries) {
+            if(!entries.Any())
+                return;
+
+            string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
+            StringBuilder sb = new();
+            sb.AppendFormat("bump#{0}", now);
+
+            Dictionary<string, string> formData = new() {
+                { "t", now },
+            };
+
+            foreach(var (remoteAddr, userId) in entries) {
+                string remoteAddrStr = remoteAddr.ToString();
+                sb.AppendFormat("#{0}:{1}", userId, remoteAddrStr);
+                formData.Add(string.Format("u[{0}]", userId), remoteAddrStr);
+            }
+
+            HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_BUMP_USERS_ONLINE_URL, BaseURL)) {
+                Headers = {
+                    { "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
+                },
+                Content = new FormUrlEncodedContent(formData),
+            };
+
+            using HttpResponseMessage response = await httpClient.SendAsync(request);
+            response.EnsureSuccessStatusCode();
+        }
+
+        private const string BANS_CREATE_URL = "{0}/bans/create";
+        private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";
+
+        public async Task BanCreateAsync(
+            BanKind kind,
+            TimeSpan duration,
+            IPAddress remoteAddr,
+            string? userId = null,
+            string? reason = null,
+            IPAddress? issuerRemoteAddr = null,
+            string? issuerUserId = null
+        ) {
+            if(duration <= TimeSpan.Zero || kind != BanKind.User)
+                return;
+
+            issuerUserId ??= string.Empty;
+            userId ??= string.Empty;
+            reason ??= string.Empty;
+            issuerRemoteAddr ??= IPAddress.IPv6None;
+
+            string isPerma = duration == TimeSpan.MaxValue ? "1" : "0";
+            string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString();
+            string remoteAddrStr = remoteAddr.ToString();
+            string issuerRemoteAddrStr = issuerRemoteAddr.ToString();
+
+            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
+            string sig = string.Format(
+                BANS_CREATE_SIG,
+                now, userId, remoteAddrStr,
+                issuerUserId, issuerRemoteAddrStr,
+                durationStr, isPerma, reason
+            );
+
+            HttpRequestMessage request = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) {
+                Headers = {
+                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
+                },
+                Content = new FormUrlEncodedContent(new Dictionary<string, string> {
+                    { "t", now },
+                    { "ui", userId },
+                    { "ua", remoteAddrStr },
+                    { "mi", issuerUserId },
+                    { "ma", issuerRemoteAddrStr },
+                    { "d", durationStr },
+                    { "p", isPerma },
+                    { "r", reason },
+                }),
+            };
+
+            using HttpResponseMessage response = await httpClient.SendAsync(request);
+
+            response.EnsureSuccessStatusCode();
+        }
+
+        private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}";
+        private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}";
+
+        public async Task<bool> BanRevokeAsync(BanInfo info) {
+            string type;
+            string target;
+
+            if(info is UserBanInfo ubi) {
+                if(info.Kind != BanKind.User)
+                    throw new ArgumentException("info argument is an instance of UserBanInfo but Kind was not set to BanKind.User", nameof(info));
+
+                type = "user";
+                target = ubi.UserId;
+            } else if(info is IPAddressBanInfo iabi) {
+                if(info.Kind != BanKind.IPAddress)
+                    throw new ArgumentException("info argument is an instance of IPAddressBanInfo but Kind was not set to BanKind.IPAddress", nameof(info));
+
+                type = "addr";
+                target = iabi.Address.ToString();
+            } else throw new ArgumentException("info argument is set to unsupported implementation", nameof(info));
+
+            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
+            string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now));
+            string sig = string.Format(BANS_REVOKE_SIG, now, type, target);
+
+            HttpRequestMessage request = new(HttpMethod.Delete, url) {
+                Headers = {
+                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
+                },
+            };
+
+            using HttpResponseMessage response = await httpClient.SendAsync(request);
+            if(response.StatusCode == HttpStatusCode.NotFound)
+                return false;
+
+            response.EnsureSuccessStatusCode();
+
+            return response.StatusCode == HttpStatusCode.NoContent;
+        }
+
+        private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}";
+        private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}";
+
+        public async Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null) {
+            userIdOrName ??= "0";
+            remoteAddr ??= IPAddress.None;
+
+            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
+            bool usingUserName = string.IsNullOrEmpty(userIdOrName) || userIdOrName.Any(c => c is < '0' or > '9');
+            string remoteAddrStr = remoteAddr.ToString();
+            string usingUserNameStr = usingUserName ? "1" : "0";
+            string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userIdOrName), Uri.EscapeDataString(remoteAddrStr), Uri.EscapeDataString(now), Uri.EscapeDataString(usingUserNameStr));
+            string sig = string.Format(BANS_CHECK_SIG, now, userIdOrName, remoteAddrStr, usingUserNameStr);
+
+            HttpRequestMessage request = new(HttpMethod.Get, url) {
+                Headers = {
+                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
+                },
+            };
+
+            using HttpResponseMessage response = await httpClient.SendAsync(request);
+            response.EnsureSuccessStatusCode();
+
+            using Stream stream = await response.Content.ReadAsStreamAsync();
+            FlashiiRawBanInfo? rawBanInfo = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo>(stream);
+            if(rawBanInfo?.IsBanned != true || rawBanInfo.HasExpired)
+                return null;
+
+            return rawBanInfo.RemoteAddress is null or "::"
+                ? new FlashiiUserBanInfo(rawBanInfo)
+                : new FlashiiIPAddressBanInfo(rawBanInfo);
+        }
+
+        private const string BANS_LIST_URL = "{0}/bans/list?x={1}";
+        private const string BANS_LIST_SIG = "list#{0}";
+
+        public async Task<BanInfo[]> BanGetListAsync() {
+            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
+            string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
+            string sig = string.Format(BANS_LIST_SIG, now);
+
+            HttpRequestMessage request = new(HttpMethod.Get, url) {
+                Headers = {
+                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
+                },
+            };
+
+            using HttpResponseMessage response = await httpClient.SendAsync(request);
+            response.EnsureSuccessStatusCode();
+
+            using Stream stream = await response.Content.ReadAsStreamAsync();
+            FlashiiRawBanInfo[]? list = await JsonSerializer.DeserializeAsync<FlashiiRawBanInfo[]>(stream);
+            if(list is null || list.Length < 1)
+                return [];
+
+            return [.. list.Where(b => b?.IsBanned == true && !b.HasExpired).Select(b => {
+                return (BanInfo)(b.RemoteAddress is null or "::"
+                    ? new FlashiiUserBanInfo(b) : new FlashiiIPAddressBanInfo(b));
+            })];
+        }
+    }
+}
diff --git a/SharpChat.Misuzu/FlashiiIPAddressBanInfo.cs b/SharpChat.Misuzu/FlashiiIPAddressBanInfo.cs
new file mode 100644
index 0000000..7bd6fc5
--- /dev/null
+++ b/SharpChat.Misuzu/FlashiiIPAddressBanInfo.cs
@@ -0,0 +1,9 @@
+using SharpChat.Bans;
+using System.Net;
+
+namespace SharpChat.Flashii {
+    public class FlashiiIPAddressBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.IPAddress, rawBanInfo), IPAddressBanInfo {
+        public IPAddress Address { get; } = IPAddress.TryParse(rawBanInfo.RemoteAddress, out IPAddress? addr) && addr is not null ? addr : IPAddress.IPv6None;
+        public override string ToString() => Address.ToString();
+    }
+}
diff --git a/SharpChat/Misuzu/MisuzuBanInfo.cs b/SharpChat.Misuzu/FlashiiRawBanInfo.cs
similarity index 88%
rename from SharpChat/Misuzu/MisuzuBanInfo.cs
rename to SharpChat.Misuzu/FlashiiRawBanInfo.cs
index 7012b87..3263fde 100644
--- a/SharpChat/Misuzu/MisuzuBanInfo.cs
+++ b/SharpChat.Misuzu/FlashiiRawBanInfo.cs
@@ -1,13 +1,20 @@
 using System.Text.Json.Serialization;
 
-namespace SharpChat.Misuzu {
-    public class MisuzuBanInfo {
+namespace SharpChat.Flashii {
+    public class FlashiiRawBanInfo {
         [JsonPropertyName("is_ban")]
         public bool IsBanned { get; set; }
 
         [JsonPropertyName("user_id")]
         public string? UserId { get; set; }
 
+        [JsonPropertyName("user_name")]
+        public string? UserName { get; set; }
+
+        [JsonPropertyName("user_colour")]
+        public int UserColourRaw { get; set; }
+        public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw);
+
         [JsonPropertyName("ip_addr")]
         public string? RemoteAddress { get; set; }
 
@@ -17,15 +24,6 @@ namespace SharpChat.Misuzu {
         [JsonPropertyName("expires")]
         public DateTimeOffset ExpiresAt { get; set; }
 
-        // only populated in list request
-        [JsonPropertyName("user_name")]
-        public string? UserName { get; set; }
-
-        [JsonPropertyName("user_colour")]
-        public int UserColourRaw { get; set; }
-
         public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
-
-        public ColourInheritable UserColour => ColourInheritable.FromMisuzu(UserColourRaw);
     }
 }
diff --git a/SharpChat.Misuzu/FlashiiUserBanInfo.cs b/SharpChat.Misuzu/FlashiiUserBanInfo.cs
new file mode 100644
index 0000000..6124547
--- /dev/null
+++ b/SharpChat.Misuzu/FlashiiUserBanInfo.cs
@@ -0,0 +1,10 @@
+using SharpChat.Bans;
+
+namespace SharpChat.Flashii {
+    public class FlashiiUserBanInfo(FlashiiRawBanInfo rawBanInfo) : FlashiiBanInfo(BanKind.User, rawBanInfo), UserBanInfo {
+        public string UserId { get; } = rawBanInfo.UserId ?? string.Empty;
+        public string UserName { get; } = rawBanInfo.UserName ?? $"({rawBanInfo.UserId ?? string.Empty})";
+        public ColourInheritable UserColour { get; } = ColourInheritable.FromMisuzu(rawBanInfo.UserColourRaw);
+        public override string ToString() => UserName;
+    }
+}
diff --git a/SharpChat.Misuzu/SharpChat.Flashii.csproj b/SharpChat.Misuzu/SharpChat.Flashii.csproj
new file mode 100644
index 0000000..a4d46fe
--- /dev/null
+++ b/SharpChat.Misuzu/SharpChat.Flashii.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net9.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/SharpChat.sln b/SharpChat.sln
index 15cc935..13814a2 100644
--- a/SharpChat.sln
+++ b/SharpChat.sln
@@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChatCommon", "SharpChatCommon\SharpChatCommon.csproj", "{C8B619A7-7815-426D-B459-20EE26F7460E}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpChat.Flashii", "SharpChat.Misuzu\SharpChat.Flashii.csproj", "{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -31,6 +33,10 @@ Global
 		{C8B619A7-7815-426D-B459-20EE26F7460E}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{C8B619A7-7815-426D-B459-20EE26F7460E}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{C8B619A7-7815-426D-B459-20EE26F7460E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A9B0B652-C20F-4C62-A96A-EF7ACD2079E9}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
diff --git a/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs
index 6bef4b1..064928f 100644
--- a/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs
+++ b/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs
@@ -1,15 +1,16 @@
-using SharpChat.Config;
-using SharpChat.Misuzu;
+using SharpChat.Auth;
+using SharpChat.Bans;
+using SharpChat.Configuration;
 using SharpChat.S2CPackets;
 
 namespace SharpChat.C2SPacketHandlers {
     public class AuthC2SPacketHandler(
-        MisuzuClient msz,
+        AuthClient authClient,
+        BansClient bansClient,
         Channel defaultChannel,
         CachedValue<int> maxMsgLength,
         CachedValue<int> maxConns
     ) : C2SPacketHandler {
-        private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
         private readonly Channel DefaultChannel = defaultChannel ?? throw new ArgumentNullException(nameof(defaultChannel));
         private readonly CachedValue<int> MaxMessageLength = maxMsgLength ?? throw new ArgumentNullException(nameof(maxMsgLength));
         private readonly CachedValue<int> MaxConnections = maxConns ?? throw new ArgumentNullException(nameof(maxConns));
@@ -22,14 +23,9 @@ namespace SharpChat.C2SPacketHandlers {
             string[] args = ctx.SplitText(3);
 
             string? authMethod = args.ElementAtOrDefault(1);
-            if(string.IsNullOrWhiteSpace(authMethod)) {
-                ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
-                ctx.Connection.Dispose();
-                return;
-            }
-
             string? authToken = args.ElementAtOrDefault(2);
-            if(string.IsNullOrWhiteSpace(authToken)) {
+
+            if(string.IsNullOrWhiteSpace(authMethod) || string.IsNullOrWhiteSpace(authToken)) {
                 ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
                 ctx.Connection.Dispose();
                 return;
@@ -42,93 +38,75 @@ namespace SharpChat.C2SPacketHandlers {
             }
 
             Task.Run(async () => {
-                MisuzuAuthInfo? fai;
-                string ipAddr = ctx.Connection.RemoteAddress.ToString();
-
                 try {
-                    fai = await Misuzu.AuthVerifyAsync(authMethod, authToken, ipAddr);
-                } catch(Exception ex) {
-                    Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
-                    ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
-                    ctx.Connection.Dispose();
-#if DEBUG
-                    throw;
-#else
-                    return;
-#endif
-                }
+                    AuthResult authResult = await authClient.AuthVerifyAsync(
+                        ctx.Connection.RemoteAddress,
+                        authMethod,
+                        authToken
+                    );
 
-                if(fai?.Success != true) {
-                    Logger.Debug($"<{ctx.Connection.Id}> Auth fail: {fai?.Reason ?? "unknown"}");
-                    ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
-                    ctx.Connection.Dispose();
-                    return;
-                }
-
-                MisuzuBanInfo? fbi;
-                try {
-                    fbi = await Misuzu.CheckBanAsync(fai.UserId.ToString(), ipAddr);
-                } catch(Exception ex) {
-                    Logger.Write($"<{ctx.Connection.Id}> Failed auth ban check: {ex}");
-                    ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
-                    ctx.Connection.Dispose();
-#if DEBUG
-                    throw;
-#else
-                    return;
-#endif
-                }
-
-                if(fbi?.IsBanned == true && !fbi.HasExpired) {
-                    Logger.Write($"<{ctx.Connection.Id}> User is banned.");
-                    ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, fbi.IsPermanent ? DateTimeOffset.MaxValue : fbi.ExpiresAt));
-                    ctx.Connection.Dispose();
-                    return;
-                }
-
-                await ctx.Chat.ContextAccess.WaitAsync();
-                try {
-                    User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == fai.UserId);
-
-                    if(user == null)
-                        user = new User(
-                            fai.UserId,
-                            fai.UserName ?? $"({fai.UserId})",
-                            fai.Colour,
-                            fai.Rank,
-                            fai.Permissions
-                        );
-                    else
-                        ctx.Chat.UpdateUser(
-                            user,
-                            userName: fai.UserName ?? $"({fai.UserId})",
-                            colour: fai.Colour,
-                            rank: fai.Rank,
-                            perms: fai.Permissions
-                        );
-
-                    // Enforce a maximum amount of connections per user
-                    if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) {
-                        ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions));
+                    BanInfo? banInfo = await bansClient.BanGetAsync(authResult.UserId, ctx.Connection.RemoteAddress);
+                    if(banInfo is not null) {
+                        Logger.Write($"<{ctx.Connection.Id}> User is banned.");
+                        ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt));
                         ctx.Connection.Dispose();
                         return;
                     }
 
-                    ctx.Connection.BumpPing();
-                    ctx.Connection.User = user;
-                    ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.UserName}!"));
+                    await ctx.Chat.ContextAccess.WaitAsync();
+                    try {
+                        User? user = ctx.Chat.Users.FirstOrDefault(u => u.UserId == authResult.UserId);
 
-                    if(File.Exists("welcome.txt")) {
-                        IEnumerable<string> lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x));
-                        string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count()));
+                        if(user == null)
+                            user = new User(
+                                authResult.UserId,
+                                authResult.UserName ?? $"({authResult.UserId})",
+                                authResult.UserColour,
+                                authResult.UserRank,
+                                authResult.UserPermissions
+                            );
+                        else
+                            ctx.Chat.UpdateUser(
+                                user,
+                                userName: authResult.UserName ?? $"({authResult.UserId})",
+                                colour: authResult.UserColour,
+                                rank: authResult.UserRank,
+                                perms: authResult.UserPermissions
+                            );
 
-                        if(!string.IsNullOrWhiteSpace(line))
-                            ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line));
+                        // Enforce a maximum amount of connections per user
+                        if(ctx.Chat.Connections.Count(conn => conn.User == user) >= MaxConnections) {
+                            ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.MaxSessions));
+                            ctx.Connection.Dispose();
+                            return;
+                        }
+
+                        ctx.Connection.BumpPing();
+                        ctx.Connection.User = user;
+                        ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, $"Welcome to Flashii Chat, {user.UserName}!"));
+
+                        if(File.Exists("welcome.txt")) {
+                            IEnumerable<string> lines = File.ReadAllLines("welcome.txt").Where(x => !string.IsNullOrWhiteSpace(x));
+                            string? line = lines.ElementAtOrDefault(RNG.Next(lines.Count()));
+
+                            if(!string.IsNullOrWhiteSpace(line))
+                                ctx.Connection.Send(new CommandResponseS2CPacket(0, LCR.WELCOME, false, line));
+                        }
+
+                        ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength);
+                    } finally {
+                        ctx.Chat.ContextAccess.Release();
                     }
-
-                    ctx.Chat.HandleJoin(user, DefaultChannel, ctx.Connection, MaxMessageLength);
-                } finally {
-                    ctx.Chat.ContextAccess.Release();
+                } catch(AuthFailedException ex) {
+                    Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
+                    ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
+                    ctx.Connection.Dispose();
+                    throw;
+                } catch(Exception ex) {
+                    Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
+                    ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception));
+                    ctx.Connection.Dispose();
+                    throw;
                 }
             }).Wait();
         }
diff --git a/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs
index ecec108..2046fcd 100644
--- a/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs
+++ b/SharpChat/C2SPacketHandlers/PingC2SPacketHandler.cs
@@ -1,10 +1,9 @@
-using SharpChat.Misuzu;
+using SharpChat.Auth;
 using SharpChat.S2CPackets;
+using System.Net;
 
 namespace SharpChat.C2SPacketHandlers {
-    public class PingC2SPacketHandler(MisuzuClient msz) : C2SPacketHandler {
-        private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
-
+    public class PingC2SPacketHandler(AuthClient authClient) : C2SPacketHandler {
         private readonly TimeSpan BumpInterval = TimeSpan.FromMinutes(1);
         private DateTimeOffset LastBump = DateTimeOffset.MinValue;
 
@@ -24,13 +23,13 @@ namespace SharpChat.C2SPacketHandlers {
             ctx.Chat.ContextAccess.Wait();
             try {
                 if(LastBump < DateTimeOffset.UtcNow - BumpInterval) {
-                    (string, string)[] bumpList = [.. ctx.Chat.Users
+                    (IPAddress, string)[] bumpList = [.. ctx.Chat.Users
                         .Where(u => u.Status == UserStatus.Online && ctx.Chat.Connections.Any(c => c.User == u))
-                        .Select(u => (u.UserId.ToString(), ctx.Chat.GetRemoteAddresses(u).FirstOrDefault()?.ToString() ?? string.Empty))];
+                        .Select(u => (ctx.Chat.GetRemoteAddresses(u).FirstOrDefault() ?? IPAddress.None, u.UserId))];
 
                     if(bumpList.Length > 0)
                         Task.Run(async () => {
-                            await Misuzu.BumpUsersOnlineAsync(bumpList);
+                            await authClient.AuthBumpUsersOnlineAsync(bumpList);
                         }).Wait();
 
                     LastBump = DateTimeOffset.UtcNow;
diff --git a/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs
index 3330986..eb01b43 100644
--- a/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs
+++ b/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs
@@ -1,4 +1,4 @@
-using SharpChat.Config;
+using SharpChat.Configuration;
 using SharpChat.Events;
 using SharpChat.Snowflake;
 using System.Globalization;
@@ -35,7 +35,7 @@ namespace SharpChat.C2SPacketHandlers {
                 return;
 
             // Extra validation step, not necessary at all but enforces proper formatting in SCv1.
-            if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId)
+            if(!long.TryParse(args[1], out long mUserId) || user.UserId != mUserId.ToString())
                 return;
 
             ctx.Chat.ContextAccess.Wait();
diff --git a/SharpChat/Channel.cs b/SharpChat/Channel.cs
index e1a2c1e..c23edfe 100644
--- a/SharpChat/Channel.cs
+++ b/SharpChat/Channel.cs
@@ -1,16 +1,16 @@
-namespace SharpChat {
+namespace SharpChat {
     public class Channel(
         string name,
         string password = "",
         bool isTemporary = false,
         int rank = 0,
-        long ownerId = 0
+        string ownerId = ""
     ) {
         public string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));
         public string Password { get; set; } = password ?? string.Empty;
         public bool IsTemporary { get; set; } = isTemporary;
         public int Rank { get; set; } = rank;
-        public long OwnerId { get; set; } = ownerId;
+        public string OwnerId { get; set; } = ownerId;
 
         public bool HasPassword
             => !string.IsNullOrWhiteSpace(Password);
@@ -20,7 +20,7 @@
         }
 
         public bool IsOwner(User user) {
-            return OwnerId > 0
+            return string.IsNullOrEmpty(OwnerId)
                 && user != null
                 && OwnerId == user.UserId;
         }
diff --git a/SharpChat/ClientCommands/BanListClientCommand.cs b/SharpChat/ClientCommands/BanListClientCommand.cs
index 61a4382..477e74d 100644
--- a/SharpChat/ClientCommands/BanListClientCommand.cs
+++ b/SharpChat/ClientCommands/BanListClientCommand.cs
@@ -1,10 +1,8 @@
-using SharpChat.Misuzu;
+using SharpChat.Bans;
 using SharpChat.S2CPackets;
 
 namespace SharpChat.ClientCommands {
-    public class BanListClientCommand(MisuzuClient msz) : ClientCommand {
-        private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
-
+    public class BanListClientCommand(BansClient bansClient) : ClientCommand {
         public bool IsMatch(ClientCommandContext ctx) {
             return ctx.NameEquals("bans")
                 || ctx.NameEquals("banned");
@@ -19,18 +17,15 @@ namespace SharpChat.ClientCommands {
             }
 
             Task.Run(async () => {
-                MisuzuBanInfo[]? mbis = await Misuzu.GetBanListAsync();
-                if(mbis is null)
-                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
-                else
+                try {
+                    BanInfo[] banInfos = await bansClient.BanGetListAsync();
                     ctx.Chat.SendTo(ctx.User, new BanListS2CPacket(
                         msgId,
-                        mbis.Where(mbi => mbi.IsBanned && !mbi.HasExpired)
-                            .Select(mbi => new BanListS2CPacket.Entry(
-                                BanListS2CPacket.Type.UserName, // Misuzu currently only does username bans so we can just do this
-                                mbi.UserName ?? $"({mbi.UserId})"
-                            ))
+                        banInfos.Select(bi => new BanListS2CPacket.Entry(bi.Kind, bi.ToString()))
                     ));
+                } catch(Exception) {
+                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
+                }
             }).Wait();
         }
     }
diff --git a/SharpChat/ClientCommands/KickBanClientCommand.cs b/SharpChat/ClientCommands/KickBanClientCommand.cs
index 7c4c7e8..ac50925 100644
--- a/SharpChat/ClientCommands/KickBanClientCommand.cs
+++ b/SharpChat/ClientCommands/KickBanClientCommand.cs
@@ -1,10 +1,9 @@
-using SharpChat.Misuzu;
+using SharpChat.Bans;
 using SharpChat.S2CPackets;
+using System.Net;
 
 namespace SharpChat.ClientCommands {
-    public class KickBanClientCommand(MisuzuClient msz) : ClientCommand {
-        private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
-
+    public class KickBanClientCommand(BansClient bansClient) : ClientCommand {
         public bool IsMatch(ClientCommandContext ctx) {
             return ctx.NameEquals("kick")
                 || ctx.NameEquals("ban");
@@ -53,25 +52,20 @@ namespace SharpChat.ClientCommands {
             string banReason = string.Join(' ', ctx.Args.Skip(banReasonIndex));
 
             Task.Run(async () => {
-                string userId = banUser.UserId.ToString();
-                string userIp = ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault()?.ToString() ?? string.Empty;
-
-                // obviously it makes no sense to only check for one ip address but that's current misuzu limitations
-                MisuzuBanInfo? fbi = await Misuzu.CheckBanAsync(userId, userIp);
-                if(fbi is null) {
-                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
-                    return;
-                }
-
-                if(fbi.IsBanned && !fbi.HasExpired) {
+                BanInfo? banInfo = await bansClient.BanGetAsync(banUser.UserId);
+                if(banInfo is not null) {
                     ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.KICK_NOT_ALLOWED, true, banUser.LegacyName));
                     return;
                 }
 
-                await Misuzu.CreateBanAsync(
-                    userId, userIp,
-                    ctx.User.UserId.ToString(), ctx.Connection.RemoteAddress.ToString(),
-                    duration, banReason
+                await bansClient.BanCreateAsync(
+                    BanKind.User,
+                    duration,
+                    ctx.Chat.GetRemoteAddresses(banUser).FirstOrDefault() ?? IPAddress.None,
+                    banUser.UserId,
+                    banReason,
+                    ctx.Connection.RemoteAddress,
+                    ctx.User.UserId
                 );
 
                 ctx.Chat.BanUser(banUser, duration);
diff --git a/SharpChat/ClientCommands/NickClientCommand.cs b/SharpChat/ClientCommands/NickClientCommand.cs
index 7217f98..1b8a279 100644
--- a/SharpChat/ClientCommands/NickClientCommand.cs
+++ b/SharpChat/ClientCommands/NickClientCommand.cs
@@ -1,4 +1,4 @@
-using SharpChat.S2CPackets;
+using SharpChat.S2CPackets;
 using System.Globalization;
 using System.Text;
 
@@ -24,7 +24,7 @@ namespace SharpChat.ClientCommands {
             int offset = 0;
 
             if(setOthersNick && long.TryParse(ctx.Args.FirstOrDefault(), out long targetUserId) && targetUserId > 0) {
-                targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId);
+                targetUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == targetUserId.ToString());
                 ++offset;
             }
 
diff --git a/SharpChat/ClientCommands/PardonAddressClientCommand.cs b/SharpChat/ClientCommands/PardonAddressClientCommand.cs
index 97b8425..f1769af 100644
--- a/SharpChat/ClientCommands/PardonAddressClientCommand.cs
+++ b/SharpChat/ClientCommands/PardonAddressClientCommand.cs
@@ -1,11 +1,9 @@
-using SharpChat.Misuzu;
+using SharpChat.Bans;
 using SharpChat.S2CPackets;
 using System.Net;
 
 namespace SharpChat.ClientCommands {
-    public class PardonAddressClientCommand(MisuzuClient msz) : ClientCommand {
-        private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
-
+    public class PardonAddressClientCommand(BansClient bansClient) : ClientCommand {
         public bool IsMatch(ClientCommandContext ctx) {
             return ctx.NameEquals("pardonip")
                 || ctx.NameEquals("unbanip");
@@ -28,19 +26,13 @@ namespace SharpChat.ClientCommands {
             unbanAddrTarget = unbanAddr.ToString();
 
             Task.Run(async () => {
-                MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(ipAddr: unbanAddrTarget);
-                if(banInfo is null) {
-                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
-                    return;
-                }
-
-                if(!banInfo.IsBanned || banInfo.HasExpired) {
+                BanInfo? banInfo = await bansClient.BanGetAsync(remoteAddr: unbanAddr);
+                if(banInfo?.Kind != BanKind.IPAddress) {
                     ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget));
                     return;
                 }
 
-                bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.RemoteAddress);
-                if(wasBanned)
+                if(await bansClient.BanRevokeAsync(banInfo))
                     ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanAddrTarget));
                 else
                     ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanAddrTarget));
diff --git a/SharpChat/ClientCommands/PardonUserClientCommand.cs b/SharpChat/ClientCommands/PardonUserClientCommand.cs
index 593505b..48a0e55 100644
--- a/SharpChat/ClientCommands/PardonUserClientCommand.cs
+++ b/SharpChat/ClientCommands/PardonUserClientCommand.cs
@@ -1,10 +1,9 @@
-using SharpChat.Misuzu;
+using SharpChat.Bans;
 using SharpChat.S2CPackets;
+using System.Text.Json;
 
 namespace SharpChat.ClientCommands {
-    public class PardonUserClientCommand(MisuzuClient msz) : ClientCommand {
-        private readonly MisuzuClient Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
-
+    public class PardonUserClientCommand(BansClient bansClient) : ClientCommand {
         public bool IsMatch(ClientCommandContext ctx) {
             return ctx.NameEquals("pardon")
                 || ctx.NameEquals("unban");
@@ -18,39 +17,32 @@ namespace SharpChat.ClientCommands {
                 return;
             }
 
-            bool unbanUserTargetIsName = true;
             string? unbanUserTarget = ctx.Args.FirstOrDefault();
             if(string.IsNullOrWhiteSpace(unbanUserTarget)) {
                 ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_FORMAT_ERROR));
                 return;
             }
 
+            string unbanUserDisplay = unbanUserTarget;
             User? unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.NameEquals(unbanUserTarget));
-            if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId)) {
-                unbanUserTargetIsName = false;
-                unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId);
+            if(unbanUser == null && long.TryParse(unbanUserTarget, out long unbanUserId))
+                unbanUser = ctx.Chat.Users.FirstOrDefault(u => u.UserId == unbanUserId.ToString());
+            if(unbanUser != null) {
+                unbanUserTarget = unbanUser.UserId;
+                unbanUserDisplay = unbanUser.UserName;
             }
 
-            if(unbanUser != null)
-                unbanUserTarget = unbanUser.UserId.ToString();
-
             Task.Run(async () => {
-                MisuzuBanInfo? banInfo = await Misuzu.CheckBanAsync(unbanUserTarget, userIdIsName: unbanUserTargetIsName);
-                if(banInfo is null) {
-                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.GENERIC_ERROR, true));
+                BanInfo? banInfo = await bansClient.BanGetAsync(unbanUserTarget);
+                if(banInfo?.Kind != BanKind.User) {
+                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay));
                     return;
                 }
 
-                if(!banInfo.IsBanned || banInfo.HasExpired) {
-                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserTarget));
-                    return;
-                }
-
-                bool wasBanned = await Misuzu.RevokeBanAsync(banInfo, MisuzuClient.BanRevokeKind.UserId);
-                if(wasBanned)
-                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserTarget));
+                if(await bansClient.BanRevokeAsync(banInfo))
+                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_UNBANNED, false, unbanUserDisplay));
                 else
-                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserTarget));
+                    ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.USER_NOT_BANNED, true, unbanUserDisplay));
             }).Wait();
         }
     }
diff --git a/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs b/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs
index 30b6e60..6d1001a 100644
--- a/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs
+++ b/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs
@@ -1,4 +1,4 @@
-using SharpChat.S2CPackets;
+using SharpChat.S2CPackets;
 
 namespace SharpChat.ClientCommands {
     public class ShutdownRestartClientCommand(ManualResetEvent waitHandle, Func<bool> shutdownCheck) : ClientCommand {
@@ -11,7 +11,7 @@ namespace SharpChat.ClientCommands {
         }
 
         public void Dispatch(ClientCommandContext ctx) {
-            if(ctx.User.UserId != 1) {
+            if(!ctx.User.UserId.Equals("1")) {
                 long msgId = ctx.Chat.RandomSnowflake.Next();
                 ctx.Chat.SendTo(ctx.User, new CommandResponseS2CPacket(msgId, LCR.COMMAND_NOT_ALLOWED, true, $"/{ctx.Name}"));
                 return;
diff --git a/SharpChat/Context.cs b/SharpChat/Context.cs
index e66332b..58c21a8 100644
--- a/SharpChat/Context.cs
+++ b/SharpChat/Context.cs
@@ -1,4 +1,4 @@
-using SharpChat.Events;
+using SharpChat.Events;
 using SharpChat.EventStorage;
 using SharpChat.S2CPackets;
 using SharpChat.Snowflake;
@@ -6,7 +6,7 @@ using System.Net;
 
 namespace SharpChat {
     public class Context {
-        public record ChannelUserAssoc(long UserId, string ChannelName);
+        public record ChannelUserAssoc(string UserId, string ChannelName);
 
         public readonly SemaphoreSlim ContextAccess = new(1, 1);
 
@@ -18,8 +18,8 @@ namespace SharpChat {
         public HashSet<User> Users { get; } = [];
         public EventStorage.EventStorage Events { get; }
         public HashSet<ChannelUserAssoc> ChannelUsers { get; } = [];
-        public Dictionary<long, RateLimiter> UserRateLimiters { get; } = [];
-        public Dictionary<long, Channel> UserLastChannel { get; } = [];
+        public Dictionary<string, RateLimiter> UserRateLimiters { get; } = [];
+        public Dictionary<string, Channel> UserLastChannel { get; } = [];
 
         public Context(EventStorage.EventStorage evtStore) {
             Events = evtStore ?? throw new ArgumentNullException(nameof(evtStore));
@@ -38,7 +38,7 @@ namespace SharpChat {
                     if(!mce.ChannelName.StartsWith('@'))
                         return;
 
-                    IEnumerable<long> uids = mce.ChannelName[1..].Split('-', 3).Select(u => long.TryParse(u, out long up) ? up : -1);
+                    IEnumerable<string> uids = mce.ChannelName[1..].Split('-', 3).Select(u => (long.TryParse(u, out long up) ? up : -1).ToString());
                     if(uids.Count() != 2)
                         return;
 
@@ -120,12 +120,12 @@ namespace SharpChat {
             return [.. Channels.Where(c => names.Any(n => c.NameEquals(n)))];
         }
 
-        public long[] GetChannelUserIds(Channel channel) {
+        public string[] GetChannelUserIds(Channel channel) {
             return [.. ChannelUsers.Where(cu => channel.NameEquals(cu.ChannelName)).Select(cu => cu.UserId)];
         }
 
         public User[] GetChannelUsers(Channel channel) {
-            long[] ids = GetChannelUserIds(channel);
+            string[] ids = GetChannelUserIds(channel);
             return [.. Users.Where(u => ids.Contains(u.UserId))];
         }
 
diff --git a/SharpChat/EventStorage/EventStorage.cs b/SharpChat/EventStorage/EventStorage.cs
index 7a9e819..ae0f344 100644
--- a/SharpChat/EventStorage/EventStorage.cs
+++ b/SharpChat/EventStorage/EventStorage.cs
@@ -1,10 +1,10 @@
-namespace SharpChat.EventStorage {
+namespace SharpChat.EventStorage {
     public interface EventStorage {
         void AddEvent(
             long id,
             string type,
             string channelName,
-            long senderId,
+            string senderId,
             string senderName,
             ColourInheritable senderColour,
             int senderRank,
diff --git a/SharpChat/EventStorage/MariaDBEventStorage.cs b/SharpChat/EventStorage/MariaDBEventStorage.cs
index 22b4e7b..900b9be 100644
--- a/SharpChat/EventStorage/MariaDBEventStorage.cs
+++ b/SharpChat/EventStorage/MariaDBEventStorage.cs
@@ -1,4 +1,4 @@
-using MySqlConnector;
+using MySqlConnector;
 using System.Text;
 using System.Text.Json;
 
@@ -10,7 +10,7 @@ namespace SharpChat.EventStorage {
             long id,
             string type,
             string channelName,
-            long senderId,
+            string senderId,
             string senderName,
             ColourInheritable senderColour,
             int senderRank,
@@ -19,8 +19,6 @@ namespace SharpChat.EventStorage {
             object? data = null,
             StoredEventFlags flags = StoredEventFlags.None
         ) {
-            ArgumentNullException.ThrowIfNull(type);
-
             RunCommand(
                 "INSERT INTO `sqc_events` (`event_id`, `event_created`, `event_type`, `event_target`, `event_flags`, `event_data`"
                 + ", `event_sender`, `event_sender_name`, `event_sender_colour`, `event_sender_rank`, `event_sender_nick`, `event_sender_perms`)"
@@ -31,7 +29,7 @@ namespace SharpChat.EventStorage {
                 new MySqlParameter("target", string.IsNullOrWhiteSpace(channelName) ? null : channelName),
                 new MySqlParameter("flags", (byte)flags),
                 new MySqlParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data)),
-                new MySqlParameter("sender", senderId < 1 ? null : senderId),
+                new MySqlParameter("sender", long.TryParse(senderId, out long senderId64) && senderId64 > 0 ? senderId64 : null),
                 new MySqlParameter("sender_name", senderName),
                 new MySqlParameter("sender_colour", senderColour.ToMisuzu()),
                 new MySqlParameter("sender_rank", senderRank),
@@ -72,7 +70,7 @@ namespace SharpChat.EventStorage {
                 reader.GetInt64("event_id"),
                 Encoding.ASCII.GetString((byte[])reader["event_type"]),
                 reader.IsDBNull(reader.GetOrdinal("event_sender")) ? null : new User(
-                    reader.GetInt64("event_sender"),
+                    reader.GetInt64("event_sender").ToString(),
                     reader.IsDBNull(reader.GetOrdinal("event_sender_name")) ? string.Empty : reader.GetString("event_sender_name"),
                     ColourInheritable.FromMisuzu(reader.GetInt32("event_sender_colour")),
                     reader.GetInt32("event_sender_rank"),
diff --git a/SharpChat/EventStorage/MariaDBEventStorage_Database.cs b/SharpChat/EventStorage/MariaDBEventStorage_Database.cs
index 88e4b7e..1035626 100644
--- a/SharpChat/EventStorage/MariaDBEventStorage_Database.cs
+++ b/SharpChat/EventStorage/MariaDBEventStorage_Database.cs
@@ -1,9 +1,9 @@
-using MySqlConnector;
-using SharpChat.Config;
+using MySqlConnector;
+using SharpChat.Configuration;
 
 namespace SharpChat.EventStorage {
     public partial class MariaDBEventStorage {
-        public static string BuildConnString(Config.Config config) {
+        public static string BuildConnString(Configuration.Config config) {
             return BuildConnString(
                 config.ReadValue("host", "localhost"),
                 config.ReadValue("user", string.Empty),
diff --git a/SharpChat/EventStorage/VirtualEventStorage.cs b/SharpChat/EventStorage/VirtualEventStorage.cs
index b96c8ea..72ccc21 100644
--- a/SharpChat/EventStorage/VirtualEventStorage.cs
+++ b/SharpChat/EventStorage/VirtualEventStorage.cs
@@ -1,4 +1,4 @@
-using System.Text.Json;
+using System.Text.Json;
 
 namespace SharpChat.EventStorage {
     public class VirtualEventStorage : EventStorage {
@@ -8,7 +8,7 @@ namespace SharpChat.EventStorage {
             long id,
             string type,
             string channelName,
-            long senderId,
+            string senderId,
             string senderName,
             ColourInheritable senderColour,
             int senderRank,
@@ -17,18 +17,28 @@ namespace SharpChat.EventStorage {
             object? data = null,
             StoredEventFlags flags = StoredEventFlags.None
         ) {
-            ArgumentNullException.ThrowIfNull(type);
-
-            // VES is meant as an emergency fallback but this is something else
-            JsonDocument hack = JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data));
-            Events.Add(id, new(id, type, senderId < 1 ? null : new User(
-                senderId,
-                senderName,
-                senderColour,
-                senderRank,
-                senderPerms,
-                senderNick
-            ), DateTimeOffset.Now, null, channelName, hack, flags));
+            Events.Add(
+                id,
+                new(
+                    id,
+                    type,
+                    long.TryParse(senderId, out long senderId64) && senderId64 > 0 
+                        ? new User(
+                            senderId,
+                            senderName,
+                            senderColour,
+                            senderRank,
+                            senderPerms,
+                            senderNick
+                        )
+                        : null,
+                    DateTimeOffset.Now,
+                    null,
+                    channelName,
+                    JsonDocument.Parse(data == null ? "{}" : JsonSerializer.Serialize(data)),
+                    flags
+                )
+            );
         }
 
         public StoredEventInfo? GetEvent(long seqId) {
diff --git a/SharpChat/Events/MessageCreateEvent.cs b/SharpChat/Events/MessageCreateEvent.cs
index cf28c52..22bae88 100644
--- a/SharpChat/Events/MessageCreateEvent.cs
+++ b/SharpChat/Events/MessageCreateEvent.cs
@@ -1,8 +1,8 @@
-namespace SharpChat.Events {
+namespace SharpChat.Events {
     public class MessageCreateEvent(
         long msgId,
         string channelName,
-        long senderId,
+        string senderId,
         string senderName,
         ColourInheritable senderColour,
         int senderRank,
@@ -16,7 +16,7 @@
     ) : ChatEvent {
         public long MessageId { get; } = msgId;
         public string ChannelName { get; } = channelName;
-        public long SenderId { get; } = senderId;
+        public string SenderId { get; } = senderId;
         public string SenderName { get; } = senderName;
         public ColourInheritable SenderColour { get; } = senderColour;
         public int SenderRank { get; } = senderRank;
diff --git a/SharpChat/Misuzu/MisuzuAuthInfo.cs b/SharpChat/Misuzu/MisuzuAuthInfo.cs
deleted file mode 100644
index 424e02f..0000000
--- a/SharpChat/Misuzu/MisuzuAuthInfo.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace SharpChat.Misuzu {
-    public class MisuzuAuthInfo {
-        [JsonPropertyName("success")]
-        public bool Success { get; set; }
-
-        [JsonPropertyName("reason")]
-        public string Reason { get; set; } = "none";
-
-        [JsonPropertyName("user_id")]
-        public long UserId { get; set; }
-
-        [JsonPropertyName("username")]
-        public string? UserName { get; set; }
-
-        [JsonPropertyName("colour_raw")]
-        public int ColourRaw { get; set; }
-
-        public ColourInheritable Colour => ColourInheritable.FromMisuzu(ColourRaw);
-
-        [JsonPropertyName("hierarchy")]
-        public int Rank { get; set; }
-
-        [JsonPropertyName("perms")]
-        public UserPermissions Permissions { get; set; }
-    }
-}
diff --git a/SharpChat/Misuzu/MisuzuClient.cs b/SharpChat/Misuzu/MisuzuClient.cs
deleted file mode 100644
index 66038ee..0000000
--- a/SharpChat/Misuzu/MisuzuClient.cs
+++ /dev/null
@@ -1,241 +0,0 @@
-using SharpChat.Config;
-using System.Net;
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.Json;
-
-namespace SharpChat.Misuzu {
-    public class MisuzuClient {
-        private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat";
-        private const string DEFAULT_SECRET_KEY = "woomy";
-
-        private const string BUMP_ONLINE_URL = "{0}/bump";
-        private const string AUTH_VERIFY_URL = "{0}/verify";
-
-        private const string BANS_CHECK_URL = "{0}/bans/check?u={1}&a={2}&x={3}&n={4}";
-        private const string BANS_CREATE_URL = "{0}/bans/create";
-        private const string BANS_REVOKE_URL = "{0}/bans/revoke?t={1}&s={2}&x={3}";
-        private const string BANS_LIST_URL = "{0}/bans/list?x={1}";
-
-        private const string VERIFY_SIG = "verify#{0}#{1}#{2}";
-        private const string BANS_CHECK_SIG = "check#{0}#{1}#{2}#{3}";
-        private const string BANS_REVOKE_SIG = "revoke#{0}#{1}#{2}";
-        private const string BANS_CREATE_SIG = "create#{0}#{1}#{2}#{3}#{4}#{5}#{6}#{7}";
-        private const string BANS_LIST_SIG = "list#{0}";
-
-        private readonly HttpClient HttpClient;
-
-        private CachedValue<string> BaseURL { get; }
-        private CachedValue<string> SecretKey { get; }
-
-        public MisuzuClient(HttpClient httpClient, Config.Config config) {
-            ArgumentNullException.ThrowIfNull(config);
-            HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
-
-            BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
-            SecretKey = config.ReadCached("secret", DEFAULT_SECRET_KEY);
-        }
-
-        public string CreateStringSignature(string str) {
-            return CreateBufferSignature(Encoding.UTF8.GetBytes(str));
-        }
-
-        public string CreateBufferSignature(byte[] bytes) {
-            using HMACSHA256 algo = new(Encoding.UTF8.GetBytes(SecretKey!));
-            return string.Concat(algo.ComputeHash(bytes).Select(c => c.ToString("x2")));
-        }
-
-        public async Task<MisuzuAuthInfo?> AuthVerifyAsync(string method, string token, string ipAddr) {
-            method ??= string.Empty;
-            token ??= string.Empty;
-            ipAddr ??= string.Empty;
-
-            string sig = string.Format(VERIFY_SIG, method, token, ipAddr);
-
-            HttpRequestMessage req = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
-                Content = new FormUrlEncodedContent(new Dictionary<string, string> {
-                    { "method", method },
-                    { "token", token },
-                    { "ipaddr", ipAddr },
-                }),
-                Headers = {
-                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
-                },
-            };
-
-            using HttpResponseMessage res = await HttpClient.SendAsync(req);
-
-            return JsonSerializer.Deserialize<MisuzuAuthInfo>(
-                await res.Content.ReadAsByteArrayAsync()
-            );
-        }
-
-        public async Task BumpUsersOnlineAsync(IEnumerable<(string userId, string ipAddr)> list) {
-            ArgumentNullException.ThrowIfNull(list);
-            if(!list.Any())
-                return;
-
-            string now = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
-            StringBuilder sb = new();
-            sb.AppendFormat("bump#{0}", now);
-
-            Dictionary<string, string> formData = new() {
-                { "t", now }
-            };
-
-            foreach(var (userId, ipAddr) in list) {
-                sb.AppendFormat("#{0}:{1}", userId, ipAddr);
-                formData.Add(string.Format("u[{0}]", userId), ipAddr);
-            }
-
-            HttpRequestMessage req = new(HttpMethod.Post, string.Format(BUMP_ONLINE_URL, BaseURL)) {
-                Headers = {
-                    { "X-SharpChat-Signature", CreateStringSignature(sb.ToString()) }
-                },
-                Content = new FormUrlEncodedContent(formData),
-            };
-
-            using HttpResponseMessage res = await HttpClient.SendAsync(req);
-
-            try {
-                res.EnsureSuccessStatusCode();
-            } catch(HttpRequestException) {
-                Logger.Debug(await res.Content.ReadAsStringAsync());
-#if DEBUG
-                throw;
-#endif
-            }
-        }
-
-        public async Task<MisuzuBanInfo?> CheckBanAsync(string? userId = null, string? ipAddr = null, bool userIdIsName = false) {
-            userId ??= string.Empty;
-            ipAddr ??= string.Empty;
-
-            string userIdIsNameStr = userIdIsName ? "1" : "0";
-            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
-            string url = string.Format(BANS_CHECK_URL, BaseURL, Uri.EscapeDataString(userId), Uri.EscapeDataString(ipAddr), Uri.EscapeDataString(now), Uri.EscapeDataString(userIdIsNameStr));
-            string sig = string.Format(BANS_CHECK_SIG, now, userId, ipAddr, userIdIsNameStr);
-
-            HttpRequestMessage req = new(HttpMethod.Get, url) {
-                Headers = {
-                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
-                },
-            };
-
-            using HttpResponseMessage res = await HttpClient.SendAsync(req);
-
-            return JsonSerializer.Deserialize<MisuzuBanInfo>(
-                await res.Content.ReadAsByteArrayAsync()
-            );
-        }
-
-        public async Task<MisuzuBanInfo[]?> GetBanListAsync() {
-            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
-            string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
-            string sig = string.Format(BANS_LIST_SIG, now);
-
-            HttpRequestMessage req = new(HttpMethod.Get, url) {
-                Headers = {
-                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
-                },
-            };
-
-            using HttpResponseMessage res = await HttpClient.SendAsync(req);
-
-            return JsonSerializer.Deserialize<MisuzuBanInfo[]>(
-                await res.Content.ReadAsByteArrayAsync()
-            );
-        }
-
-        public enum BanRevokeKind {
-            UserId,
-            RemoteAddress,
-        }
-
-        public async Task<bool> RevokeBanAsync(MisuzuBanInfo banInfo, BanRevokeKind kind) {
-            ArgumentNullException.ThrowIfNull(banInfo);
-
-            string type = kind switch {
-                BanRevokeKind.UserId => "user",
-                BanRevokeKind.RemoteAddress => "addr",
-                _ => throw new ArgumentException("Invalid kind specified.", nameof(kind)),
-            };
-
-            string target = kind switch {
-                BanRevokeKind.UserId => banInfo.UserId,
-                BanRevokeKind.RemoteAddress => banInfo.RemoteAddress,
-                _ => null,
-            } ?? string.Empty;
-
-            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
-            string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now));
-            string sig = string.Format(BANS_REVOKE_SIG, now, type, target);
-
-            HttpRequestMessage req = new(HttpMethod.Delete, url) {
-                Headers = {
-                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
-                },
-            };
-
-            using HttpResponseMessage res = await HttpClient.SendAsync(req);
-
-            if(res.StatusCode == HttpStatusCode.NotFound)
-                return false;
-
-            res.EnsureSuccessStatusCode();
-
-            return res.StatusCode == HttpStatusCode.NoContent;
-        }
-
-        public async Task CreateBanAsync(
-            string targetId,
-            string targetAddr,
-            string modId,
-            string modAddr,
-            TimeSpan duration,
-            string reason
-        ) {
-            if(string.IsNullOrWhiteSpace(targetAddr))
-                throw new ArgumentNullException(nameof(targetAddr));
-            if(string.IsNullOrWhiteSpace(modAddr))
-                throw new ArgumentNullException(nameof(modAddr));
-            if(duration <= TimeSpan.Zero)
-                return;
-
-            modId ??= string.Empty;
-            targetId ??= string.Empty;
-            reason ??= string.Empty;
-
-            string isPerma = duration == TimeSpan.MaxValue ? "1" : "0";
-            string durationStr = duration == TimeSpan.MaxValue ? "-1" : duration.TotalSeconds.ToString();
-
-            string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
-            string sig = string.Format(
-                BANS_CREATE_SIG,
-                now, targetId, targetAddr,
-                modId, modAddr,
-                durationStr, isPerma, reason
-            );
-
-            HttpRequestMessage req = new(HttpMethod.Post, string.Format(BANS_CREATE_URL, BaseURL)) {
-                Headers = {
-                    { "X-SharpChat-Signature", CreateStringSignature(sig) },
-                },
-                Content = new FormUrlEncodedContent(new Dictionary<string, string> {
-                    { "t", now },
-                    { "ui", targetId },
-                    { "ua", targetAddr },
-                    { "mi", modId },
-                    { "ma", modAddr },
-                    { "d", durationStr },
-                    { "p", isPerma },
-                    { "r", reason },
-                }),
-            };
-
-            using HttpResponseMessage res = await HttpClient.SendAsync(req);
-
-            res.EnsureSuccessStatusCode();
-        }
-    }
-}
diff --git a/SharpChat/Program.cs b/SharpChat/Program.cs
index 1d1c6e9..148a43e 100644
--- a/SharpChat/Program.cs
+++ b/SharpChat/Program.cs
@@ -1,6 +1,6 @@
-using SharpChat.Config;
+using SharpChat.Configuration;
 using SharpChat.EventStorage;
-using SharpChat.Misuzu;
+using SharpChat.Flashii;
 using System.Text;
 
 namespace SharpChat {
@@ -52,7 +52,7 @@ namespace SharpChat {
 
             if(hasCancelled) return;
 
-            MisuzuClient msz = new(httpClient, config.ScopeTo("msz"));
+            FlashiiClient flashii = new(httpClient, config.ScopeTo("msz"));
 
             if(hasCancelled) return;
 
@@ -67,7 +67,7 @@ namespace SharpChat {
 
             if(hasCancelled) return;
 
-            using SockChatServer scs = new(httpClient, msz, evtStore, config.ScopeTo("chat"));
+            using SockChatServer scs = new(httpClient, flashii, flashii, evtStore, config.ScopeTo("chat"));
             scs.Listen(mre);
 
             mre.WaitOne();
diff --git a/SharpChat/S2CPackets/AuthFailS2CPacket.cs b/SharpChat/S2CPackets/AuthFailS2CPacket.cs
index 4c290a1..28b9434 100644
--- a/SharpChat/S2CPackets/AuthFailS2CPacket.cs
+++ b/SharpChat/S2CPackets/AuthFailS2CPacket.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
     public class AuthFailS2CPacket(
@@ -9,6 +9,7 @@ namespace SharpChat.S2CPackets {
             AuthInvalid,
             MaxSessions,
             Banned,
+            Exception,
         }
 
         public string Pack() {
@@ -21,6 +22,9 @@ namespace SharpChat.S2CPackets {
                 default:
                     sb.Append("authfail");
                     break;
+                case Reason.Exception:
+                    sb.Append("userfail");
+                    break;
                 case Reason.MaxSessions:
                     sb.Append("sockfail");
                     break;
diff --git a/SharpChat/S2CPackets/AuthSuccessS2CPacket.cs b/SharpChat/S2CPackets/AuthSuccessS2CPacket.cs
index dc92a39..5bdee5f 100644
--- a/SharpChat/S2CPackets/AuthSuccessS2CPacket.cs
+++ b/SharpChat/S2CPackets/AuthSuccessS2CPacket.cs
@@ -1,8 +1,8 @@
-using System.Text;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
     public class AuthSuccessS2CPacket(
-        long userId,
+        string userId,
         string userName,
         ColourInheritable userColour,
         int userRank,
diff --git a/SharpChat/S2CPackets/BanListS2CPacket.cs b/SharpChat/S2CPackets/BanListS2CPacket.cs
index e55dadd..9fc1a2e 100644
--- a/SharpChat/S2CPackets/BanListS2CPacket.cs
+++ b/SharpChat/S2CPackets/BanListS2CPacket.cs
@@ -1,16 +1,12 @@
-using System.Text;
+using SharpChat.Bans;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
     public class BanListS2CPacket(
         long msgId,
         IEnumerable<BanListS2CPacket.Entry> entries
     ) : S2CPacket {
-        public enum Type {
-            UserName,
-            IPAddress,
-        }
-
-        public record Entry(Type type, string value);
+        public record Entry(BanKind type, string value);
 
         public string Pack() {
             StringBuilder sb = new();
@@ -20,7 +16,7 @@ namespace SharpChat.S2CPackets {
             sb.Append("\t-1\t0\fbanlist\f");
             sb.Append(string.Join(", ", entries.Select(entry => string.Format(
                 @"<a href=""javascript:void(0);"" onclick=""Chat.SendMessageWrapper('{0} '+ this.innerHTML);"">{1}</a>",
-                entry.type == Type.IPAddress ? "/unbanip" : "/unban",
+                entry.type == BanKind.IPAddress ? "/unbanip" : "/unban",
                 entry.value
             ))));
             sb.Append('\t');
diff --git a/SharpChat/S2CPackets/ChatMessageAddS2CPacket.cs b/SharpChat/S2CPackets/ChatMessageAddS2CPacket.cs
index d694194..1494de8 100644
--- a/SharpChat/S2CPackets/ChatMessageAddS2CPacket.cs
+++ b/SharpChat/S2CPackets/ChatMessageAddS2CPacket.cs
@@ -1,10 +1,10 @@
-using System.Text;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
     public class ChatMessageAddS2CPacket(
         long msgId,
         DateTimeOffset created,
-        long userId,
+        string userId,
         string text,
         bool isAction,
         bool isPrivate
diff --git a/SharpChat/S2CPackets/ContextUsersS2CPacket.cs b/SharpChat/S2CPackets/ContextUsersS2CPacket.cs
index 4d11566..2cf488c 100644
--- a/SharpChat/S2CPackets/ContextUsersS2CPacket.cs
+++ b/SharpChat/S2CPackets/ContextUsersS2CPacket.cs
@@ -1,8 +1,8 @@
-using System.Text;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
     public class ContextUsersS2CPacket(IEnumerable<ContextUsersS2CPacket.Entry> entries) : S2CPacket {
-        public record Entry(long id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible);
+        public record Entry(string id, string name, ColourInheritable colour, int rank, UserPermissions perms, bool visible);
 
         public string Pack() {
             StringBuilder sb = new();
diff --git a/SharpChat/S2CPackets/UserChannelJoinS2CPacket.cs b/SharpChat/S2CPackets/UserChannelJoinS2CPacket.cs
index f2e4f32..102a3c0 100644
--- a/SharpChat/S2CPackets/UserChannelJoinS2CPacket.cs
+++ b/SharpChat/S2CPackets/UserChannelJoinS2CPacket.cs
@@ -1,9 +1,9 @@
-using System.Text;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
     public class UserChannelJoinS2CPacket(
         long msgId,
-        long userId,
+        string userId,
         string userName,
         ColourInheritable userColour,
         int userRank,
diff --git a/SharpChat/S2CPackets/UserChannelLeaveS2CPacket.cs b/SharpChat/S2CPackets/UserChannelLeaveS2CPacket.cs
index ace8e00..b5dbb6a 100644
--- a/SharpChat/S2CPackets/UserChannelLeaveS2CPacket.cs
+++ b/SharpChat/S2CPackets/UserChannelLeaveS2CPacket.cs
@@ -1,7 +1,7 @@
-using System.Text;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
-    public class UserChannelLeaveS2CPacket(long msgId, long userId) : S2CPacket {
+    public class UserChannelLeaveS2CPacket(long msgId, string userId) : S2CPacket {
         public string Pack() {
             StringBuilder sb = new();
 
diff --git a/SharpChat/S2CPackets/UserConnectS2CPacket.cs b/SharpChat/S2CPackets/UserConnectS2CPacket.cs
index 8e04800..6c8a3a5 100644
--- a/SharpChat/S2CPackets/UserConnectS2CPacket.cs
+++ b/SharpChat/S2CPackets/UserConnectS2CPacket.cs
@@ -1,10 +1,10 @@
-using System.Text;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
     public class UserConnectS2CPacket(
         long msgId,
         DateTimeOffset joined,
-        long userId,
+        string userId,
         string userName,
         ColourInheritable userColour,
         int userRank,
diff --git a/SharpChat/S2CPackets/UserDisconnectS2CPacket.cs b/SharpChat/S2CPackets/UserDisconnectS2CPacket.cs
index a0e3ba7..5d1c6fd 100644
--- a/SharpChat/S2CPackets/UserDisconnectS2CPacket.cs
+++ b/SharpChat/S2CPackets/UserDisconnectS2CPacket.cs
@@ -1,10 +1,10 @@
-using System.Text;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
     public class UserDisconnectS2CPacket(
         long msgId,
         DateTimeOffset disconnected,
-        long userId,
+        string userId,
         string userName,
         UserDisconnectS2CPacket.Reason reason
     ) : S2CPacket {
diff --git a/SharpChat/S2CPackets/UserUpdateS2CPacket.cs b/SharpChat/S2CPackets/UserUpdateS2CPacket.cs
index 9d7cab9..cc41808 100644
--- a/SharpChat/S2CPackets/UserUpdateS2CPacket.cs
+++ b/SharpChat/S2CPackets/UserUpdateS2CPacket.cs
@@ -1,8 +1,8 @@
-using System.Text;
+using System.Text;
 
 namespace SharpChat.S2CPackets {
     public class UserUpdateS2CPacket(
-        long userId,
+        string userId,
         string userName,
         ColourInheritable userColour,
         int userRank,
diff --git a/SharpChat/SharpChat.csproj b/SharpChat/SharpChat.csproj
index df29243..5fdb581 100644
--- a/SharpChat/SharpChat.csproj
+++ b/SharpChat/SharpChat.csproj
@@ -33,6 +33,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\SharpChat.Misuzu\SharpChat.Flashii.csproj" />
     <ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
   </ItemGroup>
 
diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs
index da602b0..07b2909 100644
--- a/SharpChat/SockChatServer.cs
+++ b/SharpChat/SockChatServer.cs
@@ -1,10 +1,11 @@
-using Fleck;
-using SharpChat.ClientCommands;
-using SharpChat.Config;
-using SharpChat.EventStorage;
-using SharpChat.Misuzu;
-using SharpChat.S2CPackets;
+using Fleck;
+using SharpChat.Auth;
+using SharpChat.Bans;
 using SharpChat.C2SPacketHandlers;
+using SharpChat.ClientCommands;
+using SharpChat.Configuration;
+using SharpChat.S2CPackets;
+using System.Net;
 
 namespace SharpChat {
     public class SockChatServer : IDisposable {
@@ -18,7 +19,7 @@ namespace SharpChat {
         public Context Context { get; }
 
         private readonly HttpClient HttpClient;
-        private readonly MisuzuClient Misuzu;
+        private readonly BansClient BansClient;
 
         private readonly CachedValue<int> MaxMessageLength;
         private readonly CachedValue<int> MaxConnections;
@@ -35,11 +36,17 @@ namespace SharpChat {
 
         private Channel DefaultChannel { get; set; }
 
-        public SockChatServer(HttpClient httpClient, MisuzuClient msz, EventStorage.EventStorage evtStore, Config.Config config) {
+        public SockChatServer(
+            HttpClient httpClient,
+            AuthClient authClient,
+            BansClient bansClient,
+            EventStorage.EventStorage evtStore,
+            Config config
+        ) {
             Logger.Write("Initialising Sock Chat server...");
 
-            HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
-            Misuzu = msz ?? throw new ArgumentNullException(nameof(msz));
+            HttpClient = httpClient;
+            BansClient = bansClient;
 
             MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
             MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
@@ -51,7 +58,7 @@ namespace SharpChat {
             string[]? channelNames = config.ReadValue("channels", DEFAULT_CHANNELS);
             if(channelNames is not null)
                 foreach(string channelName in channelNames) {
-                    Config.Config channelCfg = config.ScopeTo($"channels:{channelName}");
+                    Config channelCfg = config.ScopeTo($"channels:{channelName}");
 
                     string name = channelCfg.SafeReadValue("name", string.Empty)!;
                     if(string.IsNullOrWhiteSpace(name))
@@ -70,10 +77,10 @@ namespace SharpChat {
             if(DefaultChannel is null)
                 throw new Exception("The default channel could not be determined.");
 
-            GuestHandlers.Add(new AuthC2SPacketHandler(Misuzu, DefaultChannel, MaxMessageLength, MaxConnections));
+            GuestHandlers.Add(new AuthC2SPacketHandler(authClient, bansClient, DefaultChannel, MaxMessageLength, MaxConnections));
 
             AuthedHandlers.AddRange([
-                new PingC2SPacketHandler(Misuzu),
+                new PingC2SPacketHandler(authClient),
                 SendMessageHandler = new SendMessageC2SPacketHandler(Context.RandomSnowflake, MaxMessageLength),
             ]);
 
@@ -90,10 +97,10 @@ namespace SharpChat {
                 new RankChannelClientCommand(),
                 new BroadcastClientCommand(),
                 new DeleteMessageClientCommand(),
-                new KickBanClientCommand(msz),
-                new PardonUserClientCommand(msz),
-                new PardonAddressClientCommand(msz),
-                new BanListClientCommand(msz),
+                new KickBanClientCommand(bansClient),
+                new PardonUserClientCommand(bansClient),
+                new PardonAddressClientCommand(bansClient),
+                new BanListClientCommand(bansClient),
                 new RemoteAddressClientCommand(),
             ]);
 
@@ -184,11 +191,13 @@ namespace SharpChat {
                             Context.BanUser(conn.User, banDuration, UserDisconnectS2CPacket.Reason.Flood);
 
                             if(banDuration > TimeSpan.Zero)
-                                Misuzu.CreateBanAsync(
-                                    conn.User.UserId.ToString(), conn.RemoteAddress.ToString(),
-                                    string.Empty, "::1",
+                                BansClient.BanCreateAsync(
+                                    BanKind.User,
                                     banDuration,
-                                    "Kicked from chat for flood protection."
+                                    conn.RemoteAddress,
+                                    conn.User.UserId,
+                                    "Kicked from chat for flood protection.",
+                                    IPAddress.IPv6Loopback
                                 ).Wait();
 
                             return;
diff --git a/SharpChat/User.cs b/SharpChat/User.cs
index 8fd29e9..b87c439 100644
--- a/SharpChat/User.cs
+++ b/SharpChat/User.cs
@@ -1,10 +1,10 @@
-using SharpChat.ClientCommands;
+using SharpChat.ClientCommands;
 using System.Globalization;
 using System.Text;
 
 namespace SharpChat {
     public class User(
-        long userId,
+        string userId,
         string userName,
         ColourInheritable colour,
         int rank,
@@ -17,7 +17,7 @@ namespace SharpChat {
         public const int DEFAULT_MINIMUM_DELAY = 10000;
         public const int DEFAULT_RISKY_OFFSET = 5;
 
-        public long UserId { get; } = userId;
+        public string UserId { get; } = userId;
         public string UserName { get; set; } = userName ?? throw new ArgumentNullException(nameof(userName));
         public ColourInheritable Colour { get; set; } = colour;
         public int Rank { get; set; } = rank;
@@ -65,9 +65,9 @@ namespace SharpChat {
         }
 
         public static string GetDMChannelName(User user1, User user2) {
-            return user1.UserId < user2.UserId
-                ? $"@{user1.UserId}-{user2.UserId}"
-                : $"@{user2.UserId}-{user1.UserId}";
+            return string.Compare(user1.UserId, user2.UserId, StringComparison.InvariantCultureIgnoreCase) > 0
+                ? $"@{user2.UserId}-{user1.UserId}"
+                : $"@{user1.UserId}-{user2.UserId}";
         }
     }
 }
diff --git a/SharpChatCommon/Auth/AuthClient.cs b/SharpChatCommon/Auth/AuthClient.cs
new file mode 100644
index 0000000..c0e8528
--- /dev/null
+++ b/SharpChatCommon/Auth/AuthClient.cs
@@ -0,0 +1,8 @@
+using System.Net;
+
+namespace SharpChat.Auth {
+    public interface AuthClient {
+        Task<AuthResult> AuthVerifyAsync(IPAddress remoteAddr, string scheme, string token);
+        Task AuthBumpUsersOnlineAsync(IEnumerable<(IPAddress remoteAddr, string userId)> entries);
+    }
+}
diff --git a/SharpChatCommon/Auth/AuthFailedException.cs b/SharpChatCommon/Auth/AuthFailedException.cs
new file mode 100644
index 0000000..460d00a
--- /dev/null
+++ b/SharpChatCommon/Auth/AuthFailedException.cs
@@ -0,0 +1,3 @@
+namespace SharpChat.Auth {
+    public class AuthFailedException(string message) : Exception(message) {}
+}
diff --git a/SharpChatCommon/Auth/AuthResult.cs b/SharpChatCommon/Auth/AuthResult.cs
new file mode 100644
index 0000000..95efcae
--- /dev/null
+++ b/SharpChatCommon/Auth/AuthResult.cs
@@ -0,0 +1,9 @@
+namespace SharpChat.Auth {
+    public interface AuthResult {
+        string UserId { get; }
+        string UserName { get; }
+        ColourInheritable UserColour { get; }
+        int UserRank { get; }
+        UserPermissions UserPermissions { get; }
+    }
+}
diff --git a/SharpChatCommon/Bans/BanInfo.cs b/SharpChatCommon/Bans/BanInfo.cs
new file mode 100644
index 0000000..412364e
--- /dev/null
+++ b/SharpChatCommon/Bans/BanInfo.cs
@@ -0,0 +1,9 @@
+namespace SharpChat.Bans {
+    public interface BanInfo {
+        BanKind Kind { get; }
+        bool IsPermanent { get; }
+        DateTimeOffset ExpiresAt { get; }
+        public bool HasExpired => !IsPermanent && DateTimeOffset.UtcNow >= ExpiresAt;
+        string ToString();
+    }
+}
diff --git a/SharpChatCommon/Bans/BanKind.cs b/SharpChatCommon/Bans/BanKind.cs
new file mode 100644
index 0000000..cea18cd
--- /dev/null
+++ b/SharpChatCommon/Bans/BanKind.cs
@@ -0,0 +1,6 @@
+namespace SharpChat.Bans {
+    public enum BanKind {
+        User,
+        IPAddress,
+    }
+}
diff --git a/SharpChatCommon/Bans/BansClient.cs b/SharpChatCommon/Bans/BansClient.cs
new file mode 100644
index 0000000..34b3ed7
--- /dev/null
+++ b/SharpChatCommon/Bans/BansClient.cs
@@ -0,0 +1,18 @@
+using System.Net;
+
+namespace SharpChat.Bans {
+    public interface BansClient {
+        Task BanCreateAsync(
+            BanKind kind,
+            TimeSpan duration,
+            IPAddress remoteAddr,
+            string? userId = null,
+            string? reason = null,
+            IPAddress? issuerRemoteAddr = null,
+            string? issuerUserId = null
+        );
+        Task<bool> BanRevokeAsync(BanInfo info);
+        Task<BanInfo?> BanGetAsync(string? userIdOrName = null, IPAddress? remoteAddr = null);
+        Task<BanInfo[]> BanGetListAsync();
+    }
+}
diff --git a/SharpChatCommon/Bans/IPAddressBanInfo.cs b/SharpChatCommon/Bans/IPAddressBanInfo.cs
new file mode 100644
index 0000000..ebae8e2
--- /dev/null
+++ b/SharpChatCommon/Bans/IPAddressBanInfo.cs
@@ -0,0 +1,7 @@
+using System.Net;
+
+namespace SharpChat.Bans {
+    public interface IPAddressBanInfo : BanInfo {
+        IPAddress Address { get; }
+    }
+}
diff --git a/SharpChatCommon/Bans/UserBanInfo.cs b/SharpChatCommon/Bans/UserBanInfo.cs
new file mode 100644
index 0000000..1050007
--- /dev/null
+++ b/SharpChatCommon/Bans/UserBanInfo.cs
@@ -0,0 +1,7 @@
+namespace SharpChat.Bans {
+    public interface UserBanInfo : BanInfo {
+        string UserId { get; }
+        string UserName { get; }
+        ColourInheritable UserColour { get; }
+    }
+}
diff --git a/SharpChat/Config/CachedValue.cs b/SharpChatCommon/Configuration/CachedValue.cs
similarity index 97%
rename from SharpChat/Config/CachedValue.cs
rename to SharpChatCommon/Configuration/CachedValue.cs
index 11625a1..feaf1d5 100644
--- a/SharpChat/Config/CachedValue.cs
+++ b/SharpChatCommon/Configuration/CachedValue.cs
@@ -1,4 +1,4 @@
-namespace SharpChat.Config {
+namespace SharpChat.Configuration {
     public class CachedValue<T>(Config config, string name, TimeSpan lifetime, T? fallback) {
         private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config));
         private string Name { get; } = name ?? throw new ArgumentNullException(nameof(name));
diff --git a/SharpChat/Config/Config.cs b/SharpChatCommon/Configuration/Config.cs
similarity index 96%
rename from SharpChat/Config/Config.cs
rename to SharpChatCommon/Configuration/Config.cs
index d0d60c1..04c4140 100644
--- a/SharpChat/Config/Config.cs
+++ b/SharpChatCommon/Configuration/Config.cs
@@ -1,4 +1,4 @@
-namespace SharpChat.Config {
+namespace SharpChat.Configuration {
     public interface Config : IDisposable {
         /// <summary>
         /// Creates a proxy object that forces all names to start with the given prefix.
diff --git a/SharpChat/Config/ConfigExceptions.cs b/SharpChatCommon/Configuration/ConfigExceptions.cs
similarity index 92%
rename from SharpChat/Config/ConfigExceptions.cs
rename to SharpChatCommon/Configuration/ConfigExceptions.cs
index 47f395e..6e851bf 100644
--- a/SharpChat/Config/ConfigExceptions.cs
+++ b/SharpChatCommon/Configuration/ConfigExceptions.cs
@@ -1,4 +1,4 @@
-namespace SharpChat.Config {
+namespace SharpChat.Configuration {
     public abstract class ConfigException : Exception {
         public ConfigException(string message) : base(message) { }
         public ConfigException(string message, Exception ex) : base(message, ex) { }
diff --git a/SharpChat/Config/ScopedConfig.cs b/SharpChatCommon/Configuration/ScopedConfig.cs
similarity index 97%
rename from SharpChat/Config/ScopedConfig.cs
rename to SharpChatCommon/Configuration/ScopedConfig.cs
index 56ded38..735496b 100644
--- a/SharpChat/Config/ScopedConfig.cs
+++ b/SharpChatCommon/Configuration/ScopedConfig.cs
@@ -1,4 +1,4 @@
-namespace SharpChat.Config {
+namespace SharpChat.Configuration {
     public class ScopedConfig(Config config, string prefix) : Config {
         private Config Config { get; } = config ?? throw new ArgumentNullException(nameof(config));
         private string Prefix { get; } = prefix ?? throw new ArgumentNullException(nameof(prefix));
diff --git a/SharpChat/Config/StreamConfig.cs b/SharpChatCommon/Configuration/StreamConfig.cs
similarity index 98%
rename from SharpChat/Config/StreamConfig.cs
rename to SharpChatCommon/Configuration/StreamConfig.cs
index bef573f..3c6887e 100644
--- a/SharpChat/Config/StreamConfig.cs
+++ b/SharpChatCommon/Configuration/StreamConfig.cs
@@ -1,6 +1,6 @@
-using System.Text;
+using System.Text;
 
-namespace SharpChat.Config {
+namespace SharpChat.Configuration {
     public class StreamConfig : Config {
         private Stream Stream { get; }
         private StreamReader StreamReader { get; }
diff --git a/SharpChat/UserPermissions.cs b/SharpChatCommon/UserPermissions.cs
similarity index 100%
rename from SharpChat/UserPermissions.cs
rename to SharpChatCommon/UserPermissions.cs