diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj
index 9ecb95e..6a26723 100644
--- a/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj	
+++ b/Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj	
@@ -9,20 +9,21 @@
 /* Begin PBXBuildFile section */
 		961679332CFF117300B2B6DF /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 961679322CFF117300B2B6DF /* NetworkExtension.framework */; };
 		9616793D2CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 961679302CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
-		961679532CFF207900B2B6DF /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = 961679522CFF207900B2B6DF /* SwiftProtobuf */; };
-		961679552CFF207900B2B6DF /* SwiftProtobufPluginLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 961679542CFF207900B2B6DF /* SwiftProtobufPluginLibrary */; };
 		AA3B3DA92D2D23860099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; };
-		AA3B3DB42D2D23860099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; };
-		AA3B3DB52D2D23860099996A /* VPNLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		AA3B3DBF2D2D23AB0099996A /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3DBE2D2D23AB0099996A /* SwiftProtobuf */; };
 		AA3B3DC12D2D23AB0099996A /* SwiftProtobufPluginLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3DC02D2D23AB0099996A /* SwiftProtobufPluginLibrary */; };
 		AA3B3DCD2D2D249F0099996A /* VPNLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; };
 		AA3B3DCE2D2D249F0099996A /* VPNLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		AA3B3E8E2D2E0FF40099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3E8D2D2E0FF40099996A /* Mocker */; };
+		AA3B40992D2FC8560099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
+		AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
+		AA3B40B62D2FD9DD0099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B40B52D2FD9DD0099996A /* Mocker */; };
+		AA3B40B72D2FDA5C0099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
+		AA3B40BD2D2FDFBA0099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B40BC2D2FDFBA0099996A /* Mocker */; };
+		AA3B40C02D2FE7760099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
 		AA8BC3392D0060A900E1ABAA /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC3382D0060A900E1ABAA /* ViewInspector */; };
 		AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */; };
 		AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */; };
-		AAD720D02D0816B200F6304D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = AAD720CF2D0816B200F6304D /* Alamofire */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -61,19 +62,47 @@
 			remoteGlobalIDString = 961678FB2CFF100D00B2B6DF;
 			remoteInfo = "Coder Desktop";
 		};
-		AA3B3DB22D2D23860099996A /* PBXContainerItemProxy */ = {
+		AA3B3DCF2D2D249F0099996A /* PBXContainerItemProxy */ = {
 			isa = PBXContainerItemProxy;
 			containerPortal = 961678F42CFF100D00B2B6DF /* Project object */;
 			proxyType = 1;
 			remoteGlobalIDString = AA3B3DA02D2D23860099996A;
 			remoteInfo = VPNLib;
 		};
-		AA3B3DCF2D2D249F0099996A /* PBXContainerItemProxy */ = {
+		AA3B409A2D2FC8560099996A /* PBXContainerItemProxy */ = {
 			isa = PBXContainerItemProxy;
 			containerPortal = 961678F42CFF100D00B2B6DF /* Project object */;
 			proxyType = 1;
-			remoteGlobalIDString = AA3B3DA02D2D23860099996A;
-			remoteInfo = VPNLib;
+			remoteGlobalIDString = AA3B40902D2FC8560099996A;
+			remoteInfo = CoderSDK;
+		};
+		AA3B409C2D2FC8560099996A /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 961678F42CFF100D00B2B6DF /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 961678FB2CFF100D00B2B6DF;
+			remoteInfo = "Coder Desktop";
+		};
+		AA3B40A22D2FC8560099996A /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 961678F42CFF100D00B2B6DF /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = AA3B40902D2FC8560099996A;
+			remoteInfo = CoderSDK;
+		};
+		AA3B40B92D2FDA5C0099996A /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 961678F42CFF100D00B2B6DF /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = AA3B40902D2FC8560099996A;
+			remoteInfo = CoderSDK;
+		};
+		AA3B40C22D2FE7760099996A /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 961678F42CFF100D00B2B6DF /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = AA3B40902D2FC8560099996A;
+			remoteInfo = CoderSDK;
 		};
 /* End PBXContainerItemProxy section */
 
@@ -100,17 +129,6 @@
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
 		};
-		AA3B3D922D2D233E0099996A /* Embed Frameworks */ = {
-			isa = PBXCopyFilesBuildPhase;
-			buildActionMask = 2147483647;
-			dstPath = "";
-			dstSubfolderSpec = 10;
-			files = (
-				AA3B3DB52D2D23860099996A /* VPNLib.framework in Embed Frameworks */,
-			);
-			name = "Embed Frameworks";
-			runOnlyForDeploymentPostprocessing = 0;
-		};
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
@@ -121,6 +139,8 @@
 		961679322CFF117300B2B6DF /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
 		AA3B3DA12D2D23860099996A /* VPNLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VPNLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		AA3B3DA82D2D23860099996A /* VPNLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VPNLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		AA3B40912D2FC8560099996A /* CoderSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoderSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		AA3B40982D2FC8560099996A /* CoderSDKTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoderSDKTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 /* End PBXFileReference section */
 
 /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -134,6 +154,13 @@
 			);
 			target = AA3B3DA02D2D23860099996A /* VPNLib */;
 		};
+		AA3B40A62D2FC8560099996A /* Exceptions for "CoderSDK" folder in "CoderSDK" target */ = {
+			isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+			publicHeaders = (
+				CoderSDK.h,
+			);
+			target = AA3B40902D2FC8560099996A /* CoderSDK */;
+		};
 		AA3C69C12D2D15D200A45481 /* Exceptions for "VPN" folder in "VPN" target */ = {
 			isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
 			membershipExceptions = (
@@ -172,6 +199,19 @@
 			path = VPNLibTests;
 			sourceTree = "<group>";
 		};
+		AA3B40922D2FC8560099996A /* CoderSDK */ = {
+			isa = PBXFileSystemSynchronizedRootGroup;
+			exceptions = (
+				AA3B40A62D2FC8560099996A /* Exceptions for "CoderSDK" folder in "CoderSDK" target */,
+			);
+			path = CoderSDK;
+			sourceTree = "<group>";
+		};
+		AA3B409E2D2FC8560099996A /* CoderSDKTests */ = {
+			isa = PBXFileSystemSynchronizedRootGroup;
+			path = CoderSDKTests;
+			sourceTree = "<group>";
+		};
 		AA3C69AD2D2D143400A45481 /* VPN */ = {
 			isa = PBXFileSystemSynchronizedRootGroup;
 			exceptions = (
@@ -187,12 +227,9 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				AAD720D02D0816B200F6304D /* Alamofire in Frameworks */,
-				AA3B3DB42D2D23860099996A /* VPNLib.framework in Frameworks */,
+				AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */,
 				AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */,
 				AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */,
-				961679552CFF207900B2B6DF /* SwiftProtobufPluginLibrary in Frameworks */,
-				961679532CFF207900B2B6DF /* SwiftProtobuf in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -201,6 +238,8 @@
 			buildActionMask = 2147483647;
 			files = (
 				AA8BC3392D0060A900E1ABAA /* ViewInspector in Frameworks */,
+				AA3B40B72D2FDA5C0099996A /* CoderSDK.framework in Frameworks */,
+				AA3B40B62D2FD9DD0099996A /* Mocker in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -225,6 +264,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				AA3B3DC12D2D23AB0099996A /* SwiftProtobufPluginLibrary in Frameworks */,
+				AA3B40C02D2FE7760099996A /* CoderSDK.framework in Frameworks */,
 				AA3B3DBF2D2D23AB0099996A /* SwiftProtobuf in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -238,6 +278,22 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		AA3B408E2D2FC8560099996A /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		AA3B40952D2FC8560099996A /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				AA3B40992D2FC8560099996A /* CoderSDK.framework in Frameworks */,
+				AA3B40BD2D2FDFBA0099996A /* Mocker in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
@@ -250,6 +306,8 @@
 				9616791C2CFF100E00B2B6DF /* Coder DesktopUITests */,
 				AA3B3DA22D2D23860099996A /* VPNLib */,
 				AA3B3DAE2D2D23860099996A /* VPNLibTests */,
+				AA3B40922D2FC8560099996A /* CoderSDK */,
+				AA3B409E2D2FC8560099996A /* CoderSDKTests */,
 				961679312CFF117300B2B6DF /* Frameworks */,
 				961678FD2CFF100D00B2B6DF /* Products */,
 			);
@@ -264,6 +322,8 @@
 				961679302CFF117300B2B6DF /* com.coder.Coder-Desktop.VPN.systemextension */,
 				AA3B3DA12D2D23860099996A /* VPNLib.framework */,
 				AA3B3DA82D2D23860099996A /* VPNLibTests.xctest */,
+				AA3B40912D2FC8560099996A /* CoderSDK.framework */,
+				AA3B40982D2FC8560099996A /* CoderSDKTests.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -286,6 +346,13 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		AA3B408C2D2FC8560099996A /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXHeadersBuildPhase section */
 
 /* Begin PBXNativeTarget section */
@@ -297,14 +364,13 @@
 				961678F92CFF100D00B2B6DF /* Frameworks */,
 				961678FA2CFF100D00B2B6DF /* Resources */,
 				961679422CFF117300B2B6DF /* Embed System Extensions */,
-				AA3B3D922D2D233E0099996A /* Embed Frameworks */,
 			);
 			buildRules = (
 			);
 			dependencies = (
 				AA8BC33C2D0060E700E1ABAA /* PBXTargetDependency */,
 				9616793C2CFF117300B2B6DF /* PBXTargetDependency */,
-				AA3B3DB32D2D23860099996A /* PBXTargetDependency */,
+				AA3B40A32D2FC8560099996A /* PBXTargetDependency */,
 			);
 			fileSystemSynchronizedGroups = (
 				961678FE2CFF100D00B2B6DF /* Coder Desktop */,
@@ -313,9 +379,6 @@
 			packageProductDependencies = (
 				AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */,
 				AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */,
-				AAD720CF2D0816B200F6304D /* Alamofire */,
-				961679522CFF207900B2B6DF /* SwiftProtobuf */,
-				961679542CFF207900B2B6DF /* SwiftProtobufPluginLibrary */,
 			);
 			productName = "Coder Desktop";
 			productReference = 961678FC2CFF100D00B2B6DF /* Coder Desktop.app */;
@@ -333,6 +396,7 @@
 			);
 			dependencies = (
 				961679112CFF100E00B2B6DF /* PBXTargetDependency */,
+				AA3B40BA2D2FDA5C0099996A /* PBXTargetDependency */,
 			);
 			fileSystemSynchronizedGroups = (
 				961679122CFF100E00B2B6DF /* Coder DesktopTests */,
@@ -340,6 +404,7 @@
 			name = "Coder DesktopTests";
 			packageProductDependencies = (
 				AA8BC3382D0060A900E1ABAA /* ViewInspector */,
+				AA3B40B52D2FD9DD0099996A /* Mocker */,
 			);
 			productName = "Coder DesktopTests";
 			productReference = 9616790F2CFF100E00B2B6DF /* Coder DesktopTests.xctest */;
@@ -404,6 +469,7 @@
 			buildRules = (
 			);
 			dependencies = (
+				AA3B40C32D2FE7760099996A /* PBXTargetDependency */,
 			);
 			fileSystemSynchronizedGroups = (
 				AA3B3DA22D2D23860099996A /* VPNLib */,
@@ -442,6 +508,54 @@
 			productReference = AA3B3DA82D2D23860099996A /* VPNLibTests.xctest */;
 			productType = "com.apple.product-type.bundle.unit-test";
 		};
+		AA3B40902D2FC8560099996A /* CoderSDK */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = AA3B40A72D2FC8560099996A /* Build configuration list for PBXNativeTarget "CoderSDK" */;
+			buildPhases = (
+				AA3B408C2D2FC8560099996A /* Headers */,
+				AA3B408D2D2FC8560099996A /* Sources */,
+				AA3B408E2D2FC8560099996A /* Frameworks */,
+				AA3B408F2D2FC8560099996A /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			fileSystemSynchronizedGroups = (
+				AA3B40922D2FC8560099996A /* CoderSDK */,
+			);
+			name = CoderSDK;
+			packageProductDependencies = (
+			);
+			productName = CoderSDK;
+			productReference = AA3B40912D2FC8560099996A /* CoderSDK.framework */;
+			productType = "com.apple.product-type.framework";
+		};
+		AA3B40972D2FC8560099996A /* CoderSDKTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = AA3B40AA2D2FC8560099996A /* Build configuration list for PBXNativeTarget "CoderSDKTests" */;
+			buildPhases = (
+				AA3B40942D2FC8560099996A /* Sources */,
+				AA3B40952D2FC8560099996A /* Frameworks */,
+				AA3B40962D2FC8560099996A /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				AA3B409B2D2FC8560099996A /* PBXTargetDependency */,
+				AA3B409D2D2FC8560099996A /* PBXTargetDependency */,
+			);
+			fileSystemSynchronizedGroups = (
+				AA3B409E2D2FC8560099996A /* CoderSDKTests */,
+			);
+			name = CoderSDKTests;
+			packageProductDependencies = (
+				AA3B40BC2D2FDFBA0099996A /* Mocker */,
+			);
+			productName = CoderSDKTests;
+			productReference = AA3B40982D2FC8560099996A /* CoderSDKTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
@@ -473,6 +587,13 @@
 						CreatedOnToolsVersion = 16.2;
 						TestTargetID = 961678FB2CFF100D00B2B6DF;
 					};
+					AA3B40902D2FC8560099996A = {
+						CreatedOnToolsVersion = 16.2;
+					};
+					AA3B40972D2FC8560099996A = {
+						CreatedOnToolsVersion = 16.2;
+						TestTargetID = 961678FB2CFF100D00B2B6DF;
+					};
 				};
 			};
 			buildConfigurationList = 961678F72CFF100D00B2B6DF /* Build configuration list for PBXProject "Coder Desktop" */;
@@ -489,7 +610,6 @@
 				AA8BC33A2D0060C500E1ABAA /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */,
 				AA8BC33D2D0061F200E1ABAA /* XCRemoteSwiftPackageReference "fluid-menu-bar-extra" */,
 				AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */,
-				AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */,
 				961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */,
 				AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */,
 			);
@@ -504,6 +624,8 @@
 				9616792F2CFF117300B2B6DF /* VPN */,
 				AA3B3DA02D2D23860099996A /* VPNLib */,
 				AA3B3DA72D2D23860099996A /* VPNLibTests */,
+				AA3B40902D2FC8560099996A /* CoderSDK */,
+				AA3B40972D2FC8560099996A /* CoderSDKTests */,
 			);
 		};
 /* End PBXProject section */
@@ -551,6 +673,20 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		AA3B408F2D2FC8560099996A /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		AA3B40962D2FC8560099996A /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
@@ -596,6 +732,20 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		AA3B408D2D2FC8560099996A /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		AA3B40942D2FC8560099996A /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXTargetDependency section */
@@ -624,16 +774,36 @@
 			target = 961678FB2CFF100D00B2B6DF /* Coder Desktop */;
 			targetProxy = AA3B3DAC2D2D23860099996A /* PBXContainerItemProxy */;
 		};
-		AA3B3DB32D2D23860099996A /* PBXTargetDependency */ = {
-			isa = PBXTargetDependency;
-			target = AA3B3DA02D2D23860099996A /* VPNLib */;
-			targetProxy = AA3B3DB22D2D23860099996A /* PBXContainerItemProxy */;
-		};
 		AA3B3DD02D2D249F0099996A /* PBXTargetDependency */ = {
 			isa = PBXTargetDependency;
 			target = AA3B3DA02D2D23860099996A /* VPNLib */;
 			targetProxy = AA3B3DCF2D2D249F0099996A /* PBXContainerItemProxy */;
 		};
+		AA3B409B2D2FC8560099996A /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = AA3B40902D2FC8560099996A /* CoderSDK */;
+			targetProxy = AA3B409A2D2FC8560099996A /* PBXContainerItemProxy */;
+		};
+		AA3B409D2D2FC8560099996A /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 961678FB2CFF100D00B2B6DF /* Coder Desktop */;
+			targetProxy = AA3B409C2D2FC8560099996A /* PBXContainerItemProxy */;
+		};
+		AA3B40A32D2FC8560099996A /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = AA3B40902D2FC8560099996A /* CoderSDK */;
+			targetProxy = AA3B40A22D2FC8560099996A /* PBXContainerItemProxy */;
+		};
+		AA3B40BA2D2FDA5C0099996A /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = AA3B40902D2FC8560099996A /* CoderSDK */;
+			targetProxy = AA3B40B92D2FDA5C0099996A /* PBXContainerItemProxy */;
+		};
+		AA3B40C32D2FE7760099996A /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = AA3B40902D2FC8560099996A /* CoderSDK */;
+			targetProxy = AA3B40C22D2FE7760099996A /* PBXContainerItemProxy */;
+		};
 		AA8BC33C2D0060E700E1ABAA /* PBXTargetDependency */ = {
 			isa = PBXTargetDependency;
 			productRef = AA8BC33B2D0060E700E1ABAA /* SwiftLintBuildToolPlugin */;
@@ -969,6 +1139,7 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
+				CODE_SIGN_IDENTITY = "";
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
 				CURRENT_PROJECT_VERSION = 1;
@@ -977,7 +1148,7 @@
 				DYLIB_COMPATIBILITY_VERSION = 1;
 				DYLIB_CURRENT_VERSION = 1;
 				DYLIB_INSTALL_NAME_BASE = "@rpath";
-				ENABLE_MODULE_VERIFIER = YES;
+				ENABLE_MODULE_VERIFIER = NO;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -1005,6 +1176,7 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
+				CODE_SIGN_IDENTITY = "";
 				CODE_SIGN_STYLE = Automatic;
 				COMBINE_HIDPI_IMAGES = YES;
 				CURRENT_PROJECT_VERSION = 1;
@@ -1013,7 +1185,7 @@
 				DYLIB_COMPATIBILITY_VERSION = 1;
 				DYLIB_CURRENT_VERSION = 1;
 				DYLIB_INSTALL_NAME_BASE = "@rpath";
-				ENABLE_MODULE_VERIFIER = YES;
+				ENABLE_MODULE_VERIFIER = NO;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
 				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -1071,6 +1243,114 @@
 			};
 			name = Release;
 		};
+		AA3B40A82D2FC8560099996A /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
+				CODE_SIGN_IDENTITY = "";
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				CURRENT_PROJECT_VERSION = 1;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = 4399GN35BJ;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				ENABLE_MODULE_VERIFIER = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/Frameworks",
+				);
+				MACOSX_DEPLOYMENT_TARGET = 14.6;
+				MARKETING_VERSION = 1.0;
+				MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+				MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+				PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).CoderSDK";
+				PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+				SKIP_INSTALL = YES;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_INSTALL_OBJC_HEADER = NO;
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Debug;
+		};
+		AA3B40A92D2FC8560099996A /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
+				CODE_SIGN_IDENTITY = "";
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				CURRENT_PROJECT_VERSION = 1;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = 4399GN35BJ;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				DYLIB_INSTALL_NAME_BASE = "@rpath";
+				ENABLE_MODULE_VERIFIER = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/Frameworks",
+				);
+				MACOSX_DEPLOYMENT_TARGET = 14.6;
+				MARKETING_VERSION = 1.0;
+				MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+				MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+				PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).CoderSDK";
+				PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+				SKIP_INSTALL = YES;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_INSTALL_OBJC_HEADER = NO;
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+				VERSION_INFO_PREFIX = "";
+			};
+			name = Release;
+		};
+		AA3B40AB2D2FC8560099996A /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = 4399GN35BJ;
+				GENERATE_INFOPLIST_FILE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 14.6;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.CoderSDKTests";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop";
+			};
+			name = Debug;
+		};
+		AA3B40AC2D2FC8560099996A /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = 4399GN35BJ;
+				GENERATE_INFOPLIST_FILE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 14.6;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.coder.Coder-Desktop.CoderSDKTests";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_VERSION = 5.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Coder Desktop.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Coder Desktop";
+			};
+			name = Release;
+		};
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
@@ -1137,6 +1417,24 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
+		AA3B40A72D2FC8560099996A /* Build configuration list for PBXNativeTarget "CoderSDK" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				AA3B40A82D2FC8560099996A /* Debug */,
+				AA3B40A92D2FC8560099996A /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		AA3B40AA2D2FC8560099996A /* Build configuration list for PBXNativeTarget "CoderSDKTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				AA3B40AB2D2FC8560099996A /* Debug */,
+				AA3B40AC2D2FC8560099996A /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 /* End XCConfigurationList section */
 
 /* Begin XCRemoteSwiftPackageReference section */
@@ -1188,38 +1486,30 @@
 				kind = branch;
 			};
 		};
-		AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */ = {
-			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "https://github.com/Alamofire/Alamofire";
-			requirement = {
-				kind = upToNextMajorVersion;
-				minimumVersion = 5.10.2;
-			};
-		};
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
-		961679522CFF207900B2B6DF /* SwiftProtobuf */ = {
+		AA3B3DBE2D2D23AB0099996A /* SwiftProtobuf */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */;
 			productName = SwiftProtobuf;
 		};
-		961679542CFF207900B2B6DF /* SwiftProtobufPluginLibrary */ = {
+		AA3B3DC02D2D23AB0099996A /* SwiftProtobufPluginLibrary */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */;
 			productName = SwiftProtobufPluginLibrary;
 		};
-		AA3B3DBE2D2D23AB0099996A /* SwiftProtobuf */ = {
+		AA3B3E8D2D2E0FF40099996A /* Mocker */ = {
 			isa = XCSwiftPackageProductDependency;
-			package = 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */;
-			productName = SwiftProtobuf;
+			package = AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */;
+			productName = Mocker;
 		};
-		AA3B3DC02D2D23AB0099996A /* SwiftProtobufPluginLibrary */ = {
+		AA3B40B52D2FD9DD0099996A /* Mocker */ = {
 			isa = XCSwiftPackageProductDependency;
-			package = 961679512CFF207900B2B6DF /* XCRemoteSwiftPackageReference "swift-protobuf" */;
-			productName = SwiftProtobufPluginLibrary;
+			package = AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */;
+			productName = Mocker;
 		};
-		AA3B3E8D2D2E0FF40099996A /* Mocker */ = {
+		AA3B40BC2D2FDFBA0099996A /* Mocker */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = AA3B3E8A2D2E0FE10099996A /* XCRemoteSwiftPackageReference "Mocker" */;
 			productName = Mocker;
@@ -1244,11 +1534,6 @@
 			package = AA8BC4CD2D00A4B700E1ABAA /* XCRemoteSwiftPackageReference "KeychainAccess" */;
 			productName = KeychainAccess;
 		};
-		AAD720CF2D0816B200F6304D /* Alamofire */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = AAD720CE2D0816B200F6304D /* XCRemoteSwiftPackageReference "Alamofire" */;
-			productName = Alamofire;
-		};
 /* End XCSwiftPackageProductDependency section */
 	};
 	rootObject = 961678F42CFF100D00B2B6DF /* Project object */;
diff --git a/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index ebb3ead..5a69cd2 100644
--- a/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved	
+++ b/Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved	
@@ -1,15 +1,6 @@
 {
-  "originHash" : "1cd4f7368eeddbaa35ef829e13093bc7081a4e6d3da9492d22db0757464ad473",
+  "originHash" : "ec40e522ec1a2416e8e8f5cbe97424ab3e4a614e6ef453c10ea28e84e88b6771",
   "pins" : [
-    {
-      "identity" : "alamofire",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/Alamofire/Alamofire",
-      "state" : {
-        "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
-        "version" : "5.10.2"
-      }
-    },
     {
       "identity" : "fluid-menu-bar-extra",
       "kind" : "remoteSourceControl",
diff --git a/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme b/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme
index e4f5432..c17080f 100644
--- a/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme	
+++ b/Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme	
@@ -79,6 +79,17 @@
                ReferencedContainer = "container:Coder Desktop.xcodeproj">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "AA3B40972D2FC8560099996A"
+               BuildableName = "CoderSDKTests.xctest"
+               BlueprintName = "CoderSDKTests"
+               ReferencedContainer = "container:Coder Desktop.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
       </Testables>
    </TestAction>
    <LaunchAction
diff --git a/Coder Desktop/Coder Desktop.xctestplan b/Coder Desktop/Coder Desktop.xctestplan
index 0cef4af..a0f608b 100644
--- a/Coder Desktop/Coder Desktop.xctestplan	
+++ b/Coder Desktop/Coder Desktop.xctestplan	
@@ -19,8 +19,17 @@
     {
       "target" : {
         "containerPath" : "container:Coder Desktop.xcodeproj",
-        "identifier" : "9616790E2CFF100E00B2B6DF",
-        "name" : "Coder DesktopTests"
+        "identifier" : "AA3B40972D2FC8560099996A",
+        "name" : "CoderSDKTests"
+      }
+    },
+    {
+      "enabled" : false,
+      "parallelizable" : true,
+      "target" : {
+        "containerPath" : "container:Coder Desktop.xcodeproj",
+        "identifier" : "961679182CFF100E00B2B6DF",
+        "name" : "Coder DesktopUITests"
       }
     },
     {
@@ -31,12 +40,10 @@
       }
     },
     {
-      "enabled" : false,
-      "parallelizable" : true,
       "target" : {
         "containerPath" : "container:Coder Desktop.xcodeproj",
-        "identifier" : "961679182CFF100E00B2B6DF",
-        "name" : "Coder DesktopUITests"
+        "identifier" : "9616790E2CFF100E00B2B6DF",
+        "name" : "Coder DesktopTests"
       }
     }
   ],
diff --git a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
index 408722b..7650386 100644
--- a/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift	
+++ b/Coder Desktop/Coder Desktop/Coder_DesktopApp.swift	
@@ -11,7 +11,7 @@ struct DesktopApp: App {
             EmptyView()
         }
         Window("Sign In", id: Windows.login.rawValue) {
-            LoginForm<PreviewClient, PreviewSession>()
+            LoginForm<PreviewSession>()
         }.environmentObject(appDelegate.session)
             .windowResizability(.contentSize)
     }
diff --git a/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift b/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift
deleted file mode 100644
index 7a9eef4..0000000
--- a/Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift	
+++ /dev/null
@@ -1,29 +0,0 @@
-import Alamofire
-import SwiftUI
-
-struct PreviewClient: Client {
-    init(url _: URL, token _: String? = nil) {}
-
-    func user(_: String) async throws(ClientError) -> User {
-        do {
-            try await Task.sleep(for: .seconds(1))
-            return User(
-                id: UUID(),
-                username: "admin",
-                avatar_url: "",
-                name: "admin",
-                email: "admin@coder.com",
-                created_at: Date.now,
-                updated_at: Date.now,
-                last_seen_at: Date.now,
-                status: "active",
-                login_type: "none",
-                theme_preference: "dark",
-                organization_ids: [],
-                roles: []
-            )
-        } catch {
-            throw .reqError(.explicitlyCancelled)
-        }
-    }
-}
diff --git a/Coder Desktop/Coder Desktop/SDK/Client.swift b/Coder Desktop/Coder Desktop/SDK/Client.swift
deleted file mode 100644
index 1facec2..0000000
--- a/Coder Desktop/Coder Desktop/SDK/Client.swift	
+++ /dev/null
@@ -1,140 +0,0 @@
-import Alamofire
-import Foundation
-
-protocol Client: Sendable {
-    init(url: URL, token: String?)
-    func user(_ ident: String) async throws(ClientError) -> User
-}
-
-struct CoderClient: Client {
-    public let url: URL
-    public var token: String?
-
-    static let decoder: JSONDecoder = {
-        var dec = JSONDecoder()
-        dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
-        return dec
-    }()
-
-    let encoder: JSONEncoder = {
-        var enc = JSONEncoder()
-        enc.dateEncodingStrategy = .iso8601withFractionalSeconds
-        return enc
-    }()
-
-    func request<T: Encodable & Sendable>(
-        _ path: String,
-        method: HTTPMethod,
-        body: T? = nil
-    ) async throws(ClientError) -> HTTPResponse {
-        let url = self.url.appendingPathComponent(path)
-        let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] }
-        let out = await AF.request(
-            url,
-            method: method,
-            parameters: body,
-            headers: headers
-        ).serializingData().response
-        switch out.result {
-        case let .success(data):
-            return HTTPResponse(resp: out.response!, data: data, req: out.request)
-        case let .failure(error):
-            throw .reqError(error)
-        }
-    }
-
-    func request(
-        _ path: String,
-        method: HTTPMethod
-    ) async throws(ClientError) -> HTTPResponse {
-        let url = self.url.appendingPathComponent(path)
-        let headers: HTTPHeaders? = token.map { [Headers.sessionToken: $0] }
-        let out = await AF.request(
-            url,
-            method: method,
-            headers: headers
-        ).serializingData().response
-        switch out.result {
-        case let .success(data):
-            return HTTPResponse(resp: out.response!, data: data, req: out.request)
-        case let .failure(error):
-            throw .reqError(error)
-        }
-    }
-
-    func responseAsError(_ resp: HTTPResponse) -> ClientError {
-        do {
-            let body = try CoderClient.decoder.decode(Response.self, from: resp.data)
-            let out = APIError(
-                response: body,
-                statusCode: resp.resp.statusCode,
-                method: resp.req?.httpMethod,
-                url: resp.req?.url
-            )
-            return .apiError(out)
-        } catch {
-            return .unexpectedResponse(resp.data[...1024])
-        }
-    }
-
-    enum Headers {
-        static let sessionToken = "Coder-Session-Token"
-    }
-}
-
-struct HTTPResponse {
-    let resp: HTTPURLResponse
-    let data: Data
-    let req: URLRequest?
-}
-
-struct APIError: Decodable {
-    let response: Response
-    let statusCode: Int
-    let method: String?
-    let url: URL?
-
-    var description: String {
-        var components: [String] = []
-        if let method = method, let url = url {
-            components.append("\(method) \(url.absoluteString)")
-        }
-        components.append("Unexpected status code \(statusCode):\n\(response.message)")
-        if let detail = response.detail {
-            components.append("\tError: \(detail)")
-        }
-        if let validations = response.validations, !validations.isEmpty {
-            let validationMessages = validations.map { "\t\($0.field): \($0.detail)" }
-            components.append(contentsOf: validationMessages)
-        }
-        return components.joined(separator: "\n")
-    }
-}
-
-struct Response: Decodable {
-    let message: String
-    let detail: String?
-    let validations: [FieldValidation]?
-}
-
-struct FieldValidation: Decodable {
-    let field: String
-    let detail: String
-}
-
-enum ClientError: Error {
-    case apiError(APIError)
-    case reqError(AFError)
-    case unexpectedResponse(Data)
-
-    var description: String {
-        switch self {
-        case let .apiError(error):
-            return error.description
-        case let .reqError(error):
-            return error.localizedDescription
-        case let .unexpectedResponse(data):
-            return "Unexpected response: \(data)"
-        }
-    }
-}
diff --git a/Coder Desktop/Coder Desktop/SDK/User.swift b/Coder Desktop/Coder Desktop/SDK/User.swift
deleted file mode 100644
index 9a4b906..0000000
--- a/Coder Desktop/Coder Desktop/SDK/User.swift	
+++ /dev/null
@@ -1,37 +0,0 @@
-import Foundation
-
-extension CoderClient {
-    func user(_ ident: String) async throws(ClientError) -> User {
-        let res = try await request("/api/v2/users/\(ident)", method: .get)
-        guard res.resp.statusCode == 200 else {
-            throw responseAsError(res)
-        }
-        do {
-            return try CoderClient.decoder.decode(User.self, from: res.data)
-        } catch {
-            throw .unexpectedResponse(res.data[...1024])
-        }
-    }
-}
-
-struct User: Decodable {
-    let id: UUID
-    let username: String
-    let avatar_url: String
-    let name: String
-    let email: String
-    let created_at: Date
-    let updated_at: Date
-    let last_seen_at: Date
-    let status: String
-    let login_type: String
-    let theme_preference: String
-    let organization_ids: [UUID]
-    let roles: [Role]
-}
-
-struct Role: Decodable {
-    let name: String
-    let display_name: String
-    let organization_id: UUID?
-}
diff --git a/Coder Desktop/Coder Desktop/Views/LoginForm.swift b/Coder Desktop/Coder Desktop/Views/LoginForm.swift
index ef9dbb5..a9b2e5f 100644
--- a/Coder Desktop/Coder Desktop/Views/LoginForm.swift	
+++ b/Coder Desktop/Coder Desktop/Views/LoginForm.swift	
@@ -1,6 +1,7 @@
+import CoderSDK
 import SwiftUI
 
-struct LoginForm<C: Client, S: Session>: View {
+struct LoginForm<S: Session>: View {
     @EnvironmentObject var session: S
     @Environment(\.dismiss) private var dismiss
 
@@ -69,7 +70,7 @@ struct LoginForm<C: Client, S: Session>: View {
         }
         loading = true
         defer { loading = false }
-        let client = C(url: url, token: sessionToken)
+        let client = Client(url: url, token: sessionToken)
         do {
             _ = try await client.user("me")
         } catch {
@@ -188,6 +189,6 @@ enum LoginField: Hashable {
 }
 
 #Preview {
-    LoginForm<PreviewClient, PreviewSession>()
+    LoginForm<PreviewSession>()
         .environmentObject(PreviewSession())
 }
diff --git a/Coder Desktop/Coder DesktopTests/LoginFormTests.swift b/Coder Desktop/Coder DesktopTests/LoginFormTests.swift
index ae77b5c..912f409 100644
--- a/Coder Desktop/Coder DesktopTests/LoginFormTests.swift	
+++ b/Coder Desktop/Coder DesktopTests/LoginFormTests.swift	
@@ -1,4 +1,6 @@
 @testable import Coder_Desktop
+@testable import CoderSDK
+import Mocker
 import SwiftUI
 import Testing
 import ViewInspector
@@ -7,12 +9,12 @@ import ViewInspector
 @Suite(.timeLimit(.minutes(1)))
 struct LoginTests {
     let session: MockSession
-    let sut: LoginForm<MockClient, MockSession>
+    let sut: LoginForm<MockSession>
     let view: any View
 
     init() {
         session = MockSession()
-        sut = LoginForm<MockClient, MockSession>()
+        sut = LoginForm<MockSession>()
         view = sut.environmentObject(session)
     }
 
@@ -68,14 +70,16 @@ struct LoginTests {
 
     @Test
     func testFailedAuthentication() async throws {
-        let login = LoginForm<MockErrorClient, MockSession>()
+        let login = LoginForm<MockSession>()
+        let url = URL(string: "https://testFailedAuthentication.com")!
+        Mock(url: url.appendingPathComponent("/api/v2/users/me"), statusCode: 401, data: [.get: Data()]).register()
 
         try await ViewHosting.host(login.environmentObject(session)) {
             try await login.inspection.inspect { view in
-                try view.find(ViewType.TextField.self).setInput("https://coder.example.com")
+                try view.find(ViewType.TextField.self).setInput(url.absoluteString)
                 try view.find(button: "Next").tap()
                 #expect(throws: Never.self) { try view.find(text: "Session Token") }
-                try view.find(ViewType.SecureField.self).setInput("valid-token")
+                try view.find(ViewType.SecureField.self).setInput("invalid-token")
                 try await view.actualView().submit()
                 #expect(throws: Never.self) { try view.find(ViewType.Alert.self) }
             }
@@ -84,9 +88,33 @@ struct LoginTests {
 
     @Test
     func testSuccessfulLogin() async throws {
+        let url = URL(string: "https://testSuccessfulLogin.com")!
+
+        let user = User(
+            id: UUID(),
+            username: "admin",
+            avatar_url: "",
+            name: "admin",
+            email: "admin@coder.com",
+            created_at: Date.now,
+            updated_at: Date.now,
+            last_seen_at: Date.now,
+            status: "active",
+            login_type: "none",
+            theme_preference: "dark",
+            organization_ids: [],
+            roles: []
+        )
+
+        try Mock(
+            url: url.appendingPathComponent("/api/v2/users/me"),
+            statusCode: 200,
+            data: [.get: Client.encoder.encode(user)]
+        ).register()
+
         try await ViewHosting.host(view) {
             try await sut.inspection.inspect { view in
-                try view.find(ViewType.TextField.self).setInput("https://coder.example.com")
+                try view.find(ViewType.TextField.self).setInput(url.absoluteString)
                 try view.find(button: "Next").tap()
                 try view.find(ViewType.SecureField.self).setInput("valid-token")
                 try await view.actualView().submit()
diff --git a/Coder Desktop/Coder DesktopTests/Util.swift b/Coder Desktop/Coder DesktopTests/Util.swift
index bb0ff99..244513e 100644
--- a/Coder Desktop/Coder DesktopTests/Util.swift	
+++ b/Coder Desktop/Coder DesktopTests/Util.swift	
@@ -27,7 +27,7 @@ class MockVPNService: VPNService, ObservableObject {
 
 class MockSession: Session {
     @Published
-    var hasSession: Bool = true
+    var hasSession: Bool = false
     @Published
     var sessionToken: String? = "fake-token"
     @Published
@@ -50,33 +50,4 @@ class MockSession: Session {
     }
 }
 
-struct MockClient: Client {
-    init(url _: URL, token _: String? = nil) {}
-
-    func user(_: String) async throws(ClientError) -> Coder_Desktop.User {
-        User(
-            id: UUID(),
-            username: "admin",
-            avatar_url: "",
-            name: "admin",
-            email: "admin@coder.com",
-            created_at: Date.now,
-            updated_at: Date.now,
-            last_seen_at: Date.now,
-            status: "active",
-            login_type: "none",
-            theme_preference: "dark",
-            organization_ids: [],
-            roles: []
-        )
-    }
-}
-
-struct MockErrorClient: Client {
-    init(url _: URL, token _: String?) {}
-    func user(_: String) async throws(ClientError) -> Coder_Desktop.User {
-        throw .reqError(.explicitlyCancelled)
-    }
-}
-
 extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}
diff --git a/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift b/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift
index 484cb3a..6aaf5b0 100644
--- a/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift	
+++ b/Coder Desktop/Coder DesktopTests/VPNMenuTests.swift	
@@ -104,6 +104,7 @@ struct VPNMenuTests {
 
     @Test
     func testOffWhenFailed() async throws {
+        session.hasSession = true
         try await ViewHosting.host(view) {
             try await sut.inspection.inspect { view in
                 let toggle = try view.find(ViewType.Toggle.self)
diff --git a/Coder Desktop/CoderSDK/Client.swift b/Coder Desktop/CoderSDK/Client.swift
new file mode 100644
index 0000000..59a17cb
--- /dev/null
+++ b/Coder Desktop/CoderSDK/Client.swift	
@@ -0,0 +1,137 @@
+import Foundation
+
+public struct Client {
+    public let url: URL
+    public var token: String?
+    public var headers: [HTTPHeader]
+
+    public init(url: URL, token: String? = nil, headers: [HTTPHeader] = []) {
+        self.url = url
+        self.token = token
+        self.headers = headers
+    }
+
+    static let decoder: JSONDecoder = {
+        var dec = JSONDecoder()
+        dec.dateDecodingStrategy = .iso8601withOptionalFractionalSeconds
+        return dec
+    }()
+
+    static let encoder: JSONEncoder = {
+        var enc = JSONEncoder()
+        enc.dateEncodingStrategy = .iso8601withFractionalSeconds
+        return enc
+    }()
+
+    private func doRequest(
+        path: String,
+        method: HTTPMethod,
+        body: Data? = nil
+    ) async throws(ClientError) -> HTTPResponse {
+        let url = self.url.appendingPathComponent(path)
+        var req = URLRequest(url: url)
+        if let token { req.addValue(token, forHTTPHeaderField: Headers.sessionToken) }
+        req.httpMethod = method.rawValue
+        for header in headers {
+            req.addValue(header.value, forHTTPHeaderField: header.header)
+        }
+        req.httpBody = body
+        let data: Data
+        let resp: URLResponse
+        do {
+            (data, resp) = try await URLSession.shared.data(for: req)
+        } catch {
+            throw .network(error)
+        }
+        guard let httpResponse = resp as? HTTPURLResponse else {
+            throw .unexpectedResponse(data)
+        }
+        return HTTPResponse(resp: httpResponse, data: data, req: req)
+    }
+
+    func request<T: Encodable & Sendable>(
+        _ path: String,
+        method: HTTPMethod,
+        body: T
+    ) async throws(ClientError) -> HTTPResponse {
+        let encodedBody: Data?
+        do {
+            encodedBody = try Client.encoder.encode(body)
+        } catch {
+            throw .encodeFailure(error)
+        }
+        return try await doRequest(path: path, method: method, body: encodedBody)
+    }
+
+    func request(
+        _ path: String,
+        method: HTTPMethod
+    ) async throws(ClientError) -> HTTPResponse {
+        return try await doRequest(path: path, method: method)
+    }
+
+    func responseAsError(_ resp: HTTPResponse) -> ClientError {
+        do {
+            let body = try Client.decoder.decode(Response.self, from: resp.data)
+            let out = APIError(
+                response: body,
+                statusCode: resp.resp.statusCode,
+                method: resp.req.httpMethod!,
+                url: resp.req.url!
+            )
+            return .api(out)
+        } catch {
+            return .unexpectedResponse(resp.data.prefix(1024))
+        }
+    }
+}
+
+public struct APIError: Decodable {
+    let response: Response
+    let statusCode: Int
+    let method: String
+    let url: URL
+
+    var description: String {
+        var components = ["\(method) \(url.absoluteString)\nUnexpected status code \(statusCode):\n\(response.message)"]
+        if let detail = response.detail {
+            components.append("\tError: \(detail)")
+        }
+        if let validations = response.validations, !validations.isEmpty {
+            let validationMessages = validations.map { "\t\($0.field): \($0.detail)" }
+            components.append(contentsOf: validationMessages)
+        }
+        return components.joined(separator: "\n")
+    }
+}
+
+public struct Response: Decodable {
+    let message: String
+    let detail: String?
+    let validations: [FieldValidation]?
+}
+
+public struct FieldValidation: Decodable {
+    let field: String
+    let detail: String
+}
+
+public enum ClientError: Error {
+    case api(APIError)
+    case network(any Error)
+    case unexpectedResponse(Data)
+    case encodeFailure(any Error)
+
+    public var description: String {
+        switch self {
+        case let .api(error):
+            return error.description
+        case let .network(error):
+            return error.localizedDescription
+        case let .unexpectedResponse(data):
+            return "Unexpected or non HTTP response: \(data)"
+        case let .encodeFailure(error):
+            return "Failed to encode body: \(error)"
+        }
+    }
+}
diff --git a/Coder Desktop/CoderSDK/CoderSDK.h b/Coder Desktop/CoderSDK/CoderSDK.h
new file mode 100644
index 0000000..2d4371e
--- /dev/null
+++ b/Coder Desktop/CoderSDK/CoderSDK.h	
@@ -0,0 +1,11 @@
+#import <Foundation/Foundation.h>
+
+//! Project version number for CoderSDK.
+FOUNDATION_EXPORT double CoderSDKVersionNumber;
+
+//! Project version string for CoderSDK.
+FOUNDATION_EXPORT const unsigned char CoderSDKVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import <CoderSDK/PublicHeader.h>
+
+
diff --git a/Coder Desktop/Coder Desktop/SDK/Date.swift b/Coder Desktop/CoderSDK/Date.swift
similarity index 86%
rename from Coder Desktop/Coder Desktop/SDK/Date.swift
rename to Coder Desktop/CoderSDK/Date.swift
index 05d536f..c8d7af7 100644
--- a/Coder Desktop/Coder Desktop/SDK/Date.swift	
+++ b/Coder Desktop/CoderSDK/Date.swift	
@@ -28,3 +28,9 @@ extension JSONEncoder.DateEncodingStrategy {
         try container.encode($0.formatted(.iso8601withFractionalSeconds))
     }
 }
+
+public extension Date {
+    static func == (lhs: Date, rhs: Date) -> Bool {
+        abs(lhs.timeIntervalSince1970 - rhs.timeIntervalSince1970) < 0.001
+    }
+}
diff --git a/Coder Desktop/CoderSDK/Deployment.swift b/Coder Desktop/CoderSDK/Deployment.swift
new file mode 100644
index 0000000..ea1d23c
--- /dev/null
+++ b/Coder Desktop/CoderSDK/Deployment.swift	
@@ -0,0 +1,32 @@
+public extension Client {
+    func buildInfo() async throws(ClientError) -> BuildInfoResponse {
+        let res = try await request("/api/v2/buildinfo", method: .get)
+        guard res.resp.statusCode == 200 else {
+            throw responseAsError(res)
+        }
+        do {
+            return try Client.decoder.decode(BuildInfoResponse.self, from: res.data)
+        } catch {
+            throw .unexpectedResponse(res.data.prefix(1024))
+        }
+    }
+}
+
+public struct BuildInfoResponse: Encodable, Decodable, Equatable, Sendable {
+    public let external_url: String
+    public let version: String
+    public let dashboard_url: String
+    public let telemetry: Bool
+    public let workspace_proxy: Bool
+    public let agent_api_version: String
+    public let provisioner_api_version: String
+    public let upgrade_message: String
+    public let deployment_id: String
+
+    // `version` in the form `[0-9]+.[0-9]+.[0-9]+`
+    public var semver: String? {
+        return try? NSRegularExpression(pattern: #"v(\d+\.\d+\.\d+)"#)
+            .firstMatch(in: version, range: NSRange(version.startIndex ..< version.endIndex, in: version))
+            .flatMap { Range($0.range(at: 1), in: version).map { String(version[$0]) } }
+    }
+}
diff --git a/Coder Desktop/CoderSDK/HTTP.swift b/Coder Desktop/CoderSDK/HTTP.swift
new file mode 100644
index 0000000..94b8cde
--- /dev/null
+++ b/Coder Desktop/CoderSDK/HTTP.swift	
@@ -0,0 +1,26 @@
+public struct HTTPResponse {
+    let resp: HTTPURLResponse
+    let data: Data
+    let req: URLRequest
+}
+
+public struct HTTPHeader: Sendable {
+    public let header: String
+    public let value: String
+    public init(header: String, value: String) {
+        self.header = header
+        self.value = value
+    }
+}
+
+enum HTTPMethod: String, Equatable, Hashable, Sendable {
+    case get = "GET"
+    case post = "POST"
+    case delete = "DELETE"
+    case put = "PUT"
+    case head = "HEAD"
+}
+
+enum Headers {
+    static let sessionToken = "Coder-Session-Token"
+}
diff --git a/Coder Desktop/CoderSDK/User.swift b/Coder Desktop/CoderSDK/User.swift
new file mode 100644
index 0000000..e7f85f4
--- /dev/null
+++ b/Coder Desktop/CoderSDK/User.swift	
@@ -0,0 +1,73 @@
+import Foundation
+
+public extension Client {
+    func user(_ ident: String) async throws(ClientError) -> User {
+        let res = try await request("/api/v2/users/\(ident)", method: .get)
+        guard res.resp.statusCode == 200 else {
+            throw responseAsError(res)
+        }
+        do {
+            return try Client.decoder.decode(User.self, from: res.data)
+        } catch {
+            throw .unexpectedResponse(res.data.prefix(1024))
+        }
+    }
+}
+
+public struct User: Encodable, Decodable, Equatable, Sendable {
+    public let id: UUID
+    public let username: String
+    public let avatar_url: String
+    public let name: String
+    public let email: String
+    public let created_at: Date
+    public let updated_at: Date
+    public let last_seen_at: Date
+    public let status: String
+    public let login_type: String
+    public let theme_preference: String
+    public let organization_ids: [UUID]
+    public let roles: [Role]
+
+    public init(
+        id: UUID,
+        username: String,
+        avatar_url: String,
+        name: String,
+        email: String,
+        created_at: Date,
+        updated_at: Date,
+        last_seen_at: Date,
+        status: String,
+        login_type: String,
+        theme_preference: String,
+        organization_ids: [UUID],
+        roles: [Role]
+    ) {
+        self.id = id
+        self.username = username
+        self.avatar_url = avatar_url
+        self.name = name
+        self.email = email
+        self.created_at = created_at
+        self.updated_at = updated_at
+        self.last_seen_at = last_seen_at
+        self.status = status
+        self.login_type = login_type
+        self.theme_preference = theme_preference
+        self.organization_ids = organization_ids
+        self.roles = roles
+    }
+}
+
+public struct Role: Encodable, Decodable, Equatable, Sendable {
+    public let name: String
+    public let display_name: String
+    public let organization_id: UUID?
+
+    public init(name: String, display_name: String, organization_id: UUID?) {
+        self.name = name
+        self.display_name = display_name
+        self.organization_id = organization_id
+    }
+}
diff --git a/Coder Desktop/CoderSDKTests/CoderSDKTests.swift b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift
new file mode 100644
index 0000000..33c61c4
--- /dev/null
+++ b/Coder Desktop/CoderSDKTests/CoderSDKTests.swift	
@@ -0,0 +1,76 @@
+@testable import CoderSDK
+import Mocker
+import Testing
+
+@Suite(.timeLimit(.minutes(1)))
+struct CoderSDKTests {
+    @Test
+    func user() async throws {
+        let now = Date.now
+        let user = User(
+            id: UUID(),
+            username: "johndoe",
+            avatar_url: "https://example.com/img.png",
+            name: "John Doe",
+            email: "john.doe@example.com",
+            created_at: now,
+            updated_at: now,
+            last_seen_at: now,
+            status: "active",
+            login_type: "email",
+            theme_preference: "dark",
+            organization_ids: [UUID()],
+            roles: [
+                Role(name: "user", display_name: "User", organization_id: UUID()),
+            ]
+        )
+
+        let url = URL(string: "https://example.com")!
+        let token = "fake-token"
+        let client = Client(url: url, token: token, headers: [.init(header: "X-Test-Header", value: "foo")])
+        var mock = try Mock(
+            url: url.appending(path: "api/v2/users/johndoe"),
+            contentType: .json,
+            statusCode: 200,
+            data: [.get: Client.encoder.encode(user)]
+        )
+        var correctHeaders = false
+        mock.onRequestHandler = OnRequestHandler { req in
+            correctHeaders = req.value(forHTTPHeaderField: Headers.sessionToken) == token &&
+                req.value(forHTTPHeaderField: "X-Test-Header") == "foo"
+        }
+        mock.register()
+
+        let retUser = try await client.user(user.username)
+        #expect(user == retUser)
+        #expect(correctHeaders)
+    }
+
+    @Test
+    func buildInfo() async throws {
+        let buildInfo = BuildInfoResponse(
+            external_url: "https://example.com",
+            version: "v2.18.2-devel+630fd7c0a",
+            dashboard_url: "https://example.com/dashboard",
+            telemetry: true,
+            workspace_proxy: false,
+            agent_api_version: "1.0",
+            provisioner_api_version: "1.2",
+            upgrade_message: "foo",
+            deployment_id: UUID().uuidString
+        )
+
+        let url = URL(string: "https://example.com")!
+        let client = Client(url: url)
+        try Mock(
+            url: url.appending(path: "api/v2/buildinfo"),
+            contentType: .json,
+            statusCode: 200,
+            data: [.get: Client.encoder.encode(buildInfo)]
+        ).register()
+
+        let retBuildInfo = try await client.buildInfo()
+        #expect(buildInfo == retBuildInfo)
+        #expect(retBuildInfo.semver == "2.18.2")
+    }
+}
diff --git a/Coder Desktop/VPN/Manager.swift b/Coder Desktop/VPN/Manager.swift
index d980d1c..bd598a0 100644
--- a/Coder Desktop/VPN/Manager.swift	
+++ b/Coder Desktop/VPN/Manager.swift	
@@ -1,3 +1,4 @@
+import CoderSDK
 import NetworkExtension
 import os
 import VPNLib
@@ -30,8 +31,18 @@ actor Manager {
         } catch {
             throw .download(error)
         }
+        let client = Client(url: cfg.serverUrl)
+        let buildInfo: BuildInfoResponse
         do {
-            try SignatureValidator.validate(path: dest)
+            buildInfo = try await client.buildInfo()
+        } catch {
+            throw .serverInfo(error.description)
+        }
+        guard let semver = buildInfo.semver else {
+            throw .serverInfo("invalid version: \(buildInfo.version)")
+        }
+        do {
+            try SignatureValidator.validate(path: dest, expectedVersion: semver)
         } catch {
             throw .validation(error)
         }
@@ -181,6 +192,7 @@ enum ManagerError: Error {
     case validation(ValidationError)
     case incorrectResponse(Vpn_TunnelMessage)
     case failedRPC(any Error)
+    case serverInfo(String)
     case errorResponse(msg: String)
     case noTunnelFileDescriptor
 }
diff --git a/Coder Desktop/VPNLib/Download.swift b/Coder Desktop/VPNLib/Download.swift
index 729ba05..75ff91b 100644
--- a/Coder Desktop/VPNLib/Download.swift	
+++ b/Coder Desktop/VPNLib/Download.swift	
@@ -37,7 +37,6 @@ public class SignatureValidator {
     private static let expectedName = "CoderVPN"
     private static let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib"
     private static let expectedTeamIdentifier = "4399GN35BJ"
-    private static let minDylibVersion = "2.18.1"
 
     private static let infoIdentifierKey = "CFBundleIdentifier"
     private static let infoNameKey = "CFBundleName"
@@ -45,7 +44,8 @@ public class SignatureValidator {
 
     private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation)
 
-    public static func validate(path: URL) throws(ValidationError) {
+    // `expectedVersion` must be of the form `[0-9]+.[0-9]+.[0-9]+`
+    public static func validate(path: URL, expectedVersion: String) throws(ValidationError) {
         guard FileManager.default.fileExists(atPath: path.path) else {
             throw .fileNotFound
         }
@@ -94,7 +94,7 @@ public class SignatureValidator {
         }
 
         guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
-              minDylibVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
+              expectedVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
         else {
             throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String)
         }