diff --git a/Blacklight.sln.DotSettings.user b/Blacklight.sln.DotSettings.user
index 6afa4c6e38a77283682f0bf60cc04f1f9bb21deb..b1ac598d89d1c8a8a413c7574c876ebb0e2e0d00 100644
--- a/Blacklight.sln.DotSettings.user
+++ b/Blacklight.sln.DotSettings.user
@@ -8,6 +8,7 @@
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHtmlControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F58931fa041a846b53462c4c4c5f52620bf9de09eff582aa4b1ed83c83e4e10ea_003FHtmlControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAssetLoader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fda232ecc044f4c3d8e856366253516531d2800_003F_005F27262_003FIAssetLoader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F52dbc4b2fb874f79fb4fec0b07cf7f6ca6ac95616e1d164ef210a0645a1aa9_003FIFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
+	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIToolDock_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9c3ecc55c41dc7ed59e07448c63f6e65a746b8d44d381c2de59e8b39f659a968_003FIToolDock_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANetworkInformation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5b1b048ea0e94b28ad7cbac8ef6bc4c26e00_003Fb4_003Fff405095_003FNetworkInformation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AProportionalDock_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F91c655b1795150937d9264ac1431c463ed5e5e8ad0286ad17e16f16175d94_003FProportionalDock_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
 	</wpf:ResourceDictionary>
\ No newline at end of file
diff --git a/Blacklight/App.axaml b/Blacklight/App.axaml
index abc01e617299c8294a6e9532241f3672e393349c..96cf1224b61ddd0bc6bdea995fb15f25bc17e52e 100644
--- a/Blacklight/App.axaml
+++ b/Blacklight/App.axaml
@@ -24,31 +24,7 @@
 		<Style Selector="idc|DockControl">
 			<Setter Property="(ids:DockProperties.ControlRecycling)" Value="{StaticResource ControlRecyclingKey}" />
 		</Style>
-		<Style Selector="ToolChromeControl">
-			<Setter Property="Template">
-				<ControlTemplate>
-					<Grid RowDefinitions="40,*" x:DataType="controls:IToolDock" x:CompileBindings="True" Margin="0">
-						<Border Grid.Row="0" BorderBrush="{DynamicResource ResourceExplorerBorderColor}" BorderThickness="0, 0,0, 1">
-							<Grid x:Name="PART_Grip" Background="{DynamicResource ResourceExplorerColor}">
-								<StackPanel Orientation="Horizontal" Margin="5, 2, 2, 2">
-									<Image VerticalAlignment="Center" Height="32" Width="32" Source="avares://Blacklight/Assets/t5.png" />
-									<TextBlock Margin="6,0,0,0" FontSize="14" VerticalAlignment="Center">Emilia</TextBlock>
-								</StackPanel>
 
-								<Button x:Name="PART_MenuButton" IsVisible="False" />
-								<Button x:Name="PART_PinButton" IsVisible="False" />
-								<Button x:Name="PART_MaximizeRestoreButton" IsVisible="False" />
-								<Button x:Name="PART_CloseButton" IsVisible="False" />
-							</Grid>
-						</Border>
-						<ContentPresenter x:Name="PART_ContentPresenter"
-						                  BorderThickness="0, 0, 0, 0"
-						                  Content="{TemplateBinding Content}"
-						                  Grid.Row="1" />
-					</Grid>
-				</ControlTemplate>
-			</Setter>
-		</Style>
 
 		<Style Selector="Border#PART_Border">
 			<Setter Property="BorderThickness" Value="0" />
diff --git a/Blacklight/App.axaml.cs b/Blacklight/App.axaml.cs
index 0f9ba21a945e33d702532c64d71fbc3533297e9e..06f9727bc02a2cc8da5346066ae7ba54da7711ae 100644
--- a/Blacklight/App.axaml.cs
+++ b/Blacklight/App.axaml.cs
@@ -8,6 +8,7 @@ using Avalonia.Markup.Xaml;
 using Blacklight.Util;
 using Blacklight.ViewModels;
 using Blacklight.Views;
+using Blacklight.Views.Login;
 using Lightquark.NET;
 using Microsoft.Extensions.DependencyInjection;
 
@@ -25,37 +26,48 @@ public partial class App : Application
 
     public override void OnFrameworkInitializationCompleted()
     {
-        var services = new ServiceCollection();
+        if (Services == null)
+        {
+            var services = new ServiceCollection();
 
-        services.AddSingleton<MainWindowViewModel>();
-        services.AddSingleton<ViewNavigationService>();
-        services.AddSingleton<Client>();
+            services.AddSingleton<MainWindowViewModel>();
+            services.AddSingleton<ViewNavigationService>();
+            services.AddSingleton<Client>();
         
-        services.AddSingleton<ClientViewModel>();
-        services.AddSingleton<LoginViewModel>();
+            services.AddSingleton<ClientViewModel>();
+            services.AddSingleton<LoginViewModel>();
         
-        var serviceProvider = services.BuildServiceProvider();
-        Services = serviceProvider;
+            var serviceProvider = services.BuildServiceProvider();
+            Services = serviceProvider;
 
-        serviceProvider.GetRequiredService<ViewNavigationService>();
-        var mainWindowViewModel = serviceProvider.GetRequiredService<MainWindowViewModel>();
-        var loginViewModel = serviceProvider.GetRequiredService<LoginViewModel>();
-        var client = serviceProvider.GetRequiredService<Client>();
-        client.OnLogOut = () =>
-        {
-            mainWindowViewModel.CurrentViewModel = loginViewModel;
-            var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
-            try
-            {
-                Console.WriteLine("Deleting login.json");
-                File.Delete(path);
-            }
-            catch (Exception ex)
+            serviceProvider.GetRequiredService<ViewNavigationService>();
+            var mainWindowVm = serviceProvider.GetRequiredService<MainWindowViewModel>();
+            var loginViewModel = serviceProvider.GetRequiredService<LoginViewModel>();
+            var client = serviceProvider.GetRequiredService<Client>();
+            client.OnLogOut = () =>
             {
-                Console.Error.WriteLine(ex);
-            }
-        };
-        var clientViewModel = serviceProvider.GetRequiredService<ClientViewModel>();
+                mainWindowVm.CurrentViewModel = loginViewModel;
+                loginViewModel.CurrentControl = new MainLoginPrompt();
+                var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
+                try
+                {
+                    Console.WriteLine("Deleting login.json (1)");
+                    File.Delete(path);
+                }
+                catch (Exception ex)
+                {
+                    Console.Error.WriteLine(ex);
+                }
+            };
+        }
+        else
+        {
+            Console.Error.WriteLine("Services not null when entering OnFrameworkInitializationCompleted");
+        }
+
+        var mainWindowViewModel = Services.GetRequiredService<MainWindowViewModel>();
+        var clientViewModel = Services.GetRequiredService<ClientViewModel>();
+
         
         switch (ApplicationLifetime)
         {
@@ -78,21 +90,9 @@ public partial class App : Application
                     clientViewModel.CloseLayout();
                 };
                     
-                break;
-            }
-            case ISingleViewApplicationLifetime singleViewLifetime:
-            {
-                var mainView = new ClientView
-                {
-                    DataContext = mainWindowViewModel
-                };
-
-                singleViewLifetime.MainView = mainView;
-
                 break;
             }
         }
-        
 
         base.OnFrameworkInitializationCompleted();
     }
diff --git a/Blacklight/Models/Tools/ResourceExplorer.cs b/Blacklight/Models/Tools/ResourceExplorer.cs
index 430562bf7b8b7f4236329e50ad428bbf0c6fc138..c93c7dc29a309e2a55072581f8ed6ce905848162 100644
--- a/Blacklight/Models/Tools/ResourceExplorer.cs
+++ b/Blacklight/Models/Tools/ResourceExplorer.cs
@@ -1,4 +1,4 @@
-namespace Blacklight.Models.Tools;
+namespace Blacklight.Models.Tools;
 
 public class ResourceExplorer
 {
diff --git a/Blacklight/Util/ViewNavigationService.cs b/Blacklight/Util/ViewNavigationService.cs
index 4ee93d486fc98915b9a6cbf830c4c83f812f60ba..380401c1afaf5838c2a0abb6488645f4f9108bd6 100644
--- a/Blacklight/Util/ViewNavigationService.cs
+++ b/Blacklight/Util/ViewNavigationService.cs
@@ -21,12 +21,13 @@ public class ViewNavigationService
         _mainWindowViewModel.CurrentViewModel = _serviceProvider.GetRequiredService<LoginViewModel>();
     }
 
-    public void ShowView<TViewModel>() where TViewModel : class
+    public void ShowView<TViewModel>() where TViewModel : ViewModelBase
     {
         var viewModel = _serviceProvider.GetService<TViewModel>();
         if (viewModel != null)
         {
             _mainWindowViewModel!.CurrentViewModel = viewModel;
+            viewModel.OnEnterView();
         }
         else
         {
diff --git a/Blacklight/ViewModels/ClientViewModel.cs b/Blacklight/ViewModels/ClientViewModel.cs
index c2c01f4166c40ac95fffac6230e596d0136cc961..741d1481af3898688beb913456015777ecab2bf4 100644
--- a/Blacklight/ViewModels/ClientViewModel.cs
+++ b/Blacklight/ViewModels/ClientViewModel.cs
@@ -1,4 +1,6 @@
-using System.Collections.ObjectModel;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Diagnostics;
 using System.IO;
 using System.Windows.Input;
@@ -11,6 +13,7 @@ using Dock.Model.Controls;
 using Dock.Model.Core;
 using Dock.Model.Mvvm.Controls;
 using Lightquark.NET;
+using Lightquark.NET.Objects.Reply;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Converters;
 
diff --git a/Blacklight/ViewModels/LoginViewModel.cs b/Blacklight/ViewModels/LoginViewModel.cs
index 203c6f3a309c77b7ce4914f1161cfc5861f67ce8..ea9fc3c7aaa806a3b800d7e7beb7472fa7061ce2 100644
--- a/Blacklight/ViewModels/LoginViewModel.cs
+++ b/Blacklight/ViewModels/LoginViewModel.cs
@@ -1,6 +1,7 @@
 using System;
 using System.IO;
 using System.Threading;
+using System.Threading.Tasks;
 using System.Windows.Input;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
@@ -78,11 +79,13 @@ public class LoginViewModel : ViewModelBase
         _nav = nav;
         Client = client;
         Status = "Get network";
+        Client.OnRefresh = SavePersist;
         var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
         if (Path.Exists(path))
         {
             try
             {
+                Console.WriteLine("Found login.json, loading existing data");
                 var json = File.ReadAllText(path);
                 var persistentData = JsonConvert.DeserializeObject<LoginViewModelPersistent>(json);
                 LoadExistingData(persistentData!);
@@ -94,6 +97,7 @@ public class LoginViewModel : ViewModelBase
         }
         else
         {
+            Console.WriteLine("No login.json");
             SwitchNetwork();
             _currentControl = new MainLoginPrompt();
         }
@@ -101,20 +105,31 @@ public class LoginViewModel : ViewModelBase
 
     public async void LoadExistingData(LoginViewModelPersistent persistentData)
     {
-        LoadingText = "Connecting to network";
-        var network = await Client.GetNetworkAsync(new Uri(persistentData.NetworkBase));
-        if (network.success)
+        try
         {
-            Client.UseToken(null, "temp");
-            Client.NetworkInformation = network.networkInformation;
-            Client.UseToken(persistentData.AccessToken, persistentData.RefreshToken);
-            Console.WriteLine("Found token, showing ClientView");
-            _nav.ShowView<ClientViewModel>();
-            _currentControl = new MainLoginPrompt();
+            
+            Console.WriteLine($"Connecting to {persistentData.NetworkBase} from login.json");
+            LoadingText = "Connecting to network";
+            Network = persistentData.NetworkBase;
+            var network = await Client.GetNetworkAsync(new Uri(persistentData.NetworkBase));
+            Console.WriteLine($"Fetch successful? {network.success} ({network.error})");
+            if (network.success)
+            {
+                Client.NetworkInformation = network.networkInformation;
+                Client.UseToken(persistentData.AccessToken, persistentData.RefreshToken);
+                Console.WriteLine("Found token, showing ClientView");
+                EnterClient();
+            }
+            else
+            {
+                LoadingText = $"{network.error}";
+                LoadingError = true;
+            }
         }
-        else
+        catch (UriFormatException ex)
         {
-            LoadingText = $"{network.error}";
+            Console.Error.WriteLine(ex);
+            LoadingText = $"{ex.Message}";
             LoadingError = true;
         }
     }
@@ -153,7 +168,7 @@ public class LoginViewModel : ViewModelBase
         var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
         try
         {
-            Console.WriteLine("Deleting login.json");
+            Console.WriteLine("Deleting login.json (2)");
             File.Delete(path);
         }
         catch (Exception ex)
@@ -177,16 +192,9 @@ public class LoginViewModel : ViewModelBase
         var res = await Client.AcquireToken(Email, Password);
         if (res.success)
         {
-            var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
-            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
-            await File.WriteAllTextAsync(path, JsonConvert.SerializeObject(new LoginViewModelPersistent
-            {
-                NetworkBase = Network,
-                AccessToken = res.accessToken!,
-                RefreshToken = res.refreshToken!
-            }));
-            Console.WriteLine("Logged in, showing ClientView");
-            _nav.ShowView<ClientViewModel>();
+            await SavePersist(res.accessToken!, res.refreshToken!);
+            Console.WriteLine("Logged in, entering client");
+            EnterClient();
         }
         else
         {
@@ -195,6 +203,33 @@ public class LoginViewModel : ViewModelBase
         EnableButtons = true;
     }
 
+    private async void EnterClient()
+    {
+        LoadingText = "Waiting for gateway connection";
+        LoadingError = false;
+        _currentControl = new Loading();
+        Client.GatewayConnectionEvent.Wait();
+        LoadingText = "Getting user data";
+        await Client.GetCurrentUser();
+        _nav.ShowView<ClientViewModel>();
+        LoadingText = "Loading?";
+        _currentControl = new MainLoginPrompt();
+    }
+    
+    private async Task SavePersist(string accessToken, string refreshToken)
+    {
+        Console.WriteLine($"Saving token {accessToken}");
+        var path = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight", "login.json");
+        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+        await File.WriteAllTextAsync(path, JsonConvert.SerializeObject(new LoginViewModelPersistent
+        {
+            NetworkBase = Network,
+            AccessToken = accessToken,
+            RefreshToken = refreshToken
+        }));
+        Console.WriteLine("Token saved!");
+    }
+    
     private void EnterNetworkSwitcher()
     {
         CurrentControl = new NetworkSelectionPrompt();
diff --git a/Blacklight/ViewModels/MainWindowViewModel.cs b/Blacklight/ViewModels/MainWindowViewModel.cs
index 653631d633019fe6d38f2d1d70a603d844a98151..f25cbbcc5e2b812525109e75efa460cd3683c8cb 100644
--- a/Blacklight/ViewModels/MainWindowViewModel.cs
+++ b/Blacklight/ViewModels/MainWindowViewModel.cs
@@ -4,6 +4,7 @@ using Avalonia.Controls.Shapes;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
 using Blacklight.Util;
+using Blacklight.Views.Login;
 using Lightquark.NET;
 using Microsoft.Extensions.DependencyInjection;
 using Path = System.IO.Path;
@@ -52,23 +53,8 @@ public class MainWindowViewModel : ViewModelBase
 
     public MainWindowViewModel()
     {
-        Directory.CreateDirectory(Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "blacklight"));
-        var services = new ServiceCollection();
-        services.AddSingleton<MainWindowViewModel>(_ => this);
-        services.AddSingleton<Client>();
-        services.AddSingleton<ViewNavigationService>();
-        services.AddSingleton<ClientViewModel>();
-        services.AddSingleton<LoginViewModel>();
-
-        var serviceProvider = services.BuildServiceProvider();
-        App.Services = serviceProvider;
+        var serviceProvider = App.Services!;
         var nav = serviceProvider.GetRequiredService<ViewNavigationService>();
-        var loginViewModel = serviceProvider.GetRequiredService<LoginViewModel>();
-        var client = serviceProvider.GetRequiredService<Client>();
-        client.OnLogOut = () =>
-        {
-            CurrentViewModel = loginViewModel;
-        };
         nav.SetMainWindowViewModel(this);
         _currentViewModel = serviceProvider.GetRequiredService<LoginViewModel>();
     }
diff --git a/Blacklight/ViewModels/ViewModelBase.cs b/Blacklight/ViewModels/ViewModelBase.cs
index 8a7abc6a26d4b93c2a28551b4618fe7b167689bc..290a034ba215e66c6d2c573f346e0101aeca27a3 100644
--- a/Blacklight/ViewModels/ViewModelBase.cs
+++ b/Blacklight/ViewModels/ViewModelBase.cs
@@ -4,4 +4,7 @@ namespace Blacklight.ViewModels;
 
 public class ViewModelBase : ObservableObject
 {
+    public virtual void OnEnterView()
+    {
+    }
 }
\ No newline at end of file
diff --git a/Blacklight/Views/ClientView.axaml b/Blacklight/Views/ClientView.axaml
index 83eed77992fc3aae62da22978ab737eb39b4d63b..1621953d9cc96d8b2eaf1e57644e4d15de8c0f85 100644
--- a/Blacklight/Views/ClientView.axaml
+++ b/Blacklight/Views/ClientView.axaml
@@ -7,6 +7,8 @@
              xmlns:vm="using:Blacklight.ViewModels"
              xmlns:tools="clr-namespace:Blacklight.Views.Tools"
              xmlns:viewModels="clr-namespace:Blacklight.ViewModels"
+             xmlns:controls="clr-namespace:Dock.Model.Controls;assembly=Dock.Model"
+             xmlns:views="clr-namespace:Blacklight.Views"
              mc:Ignorable="d"
              d:DesignWidth="1000" d:DesignHeight="550"
              x:CompileBindings="True"
@@ -16,6 +18,34 @@
     <vm:ClientViewModel />
   </Design.DataContext>
   
+  <UserControl.Styles>
+    <Style Selector="ToolChromeControl">
+      <Setter Property="Template">
+        <ControlTemplate >
+          <Grid RowDefinitions="40,*" x:DataType="controls:IToolDock" x:CompileBindings="True" Margin="0">
+            <Border Grid.Row="0" BorderBrush="{DynamicResource ResourceExplorerBorderColor}" BorderThickness="0, 0,0, 1">
+              <Grid x:Name="PART_Grip" Background="{DynamicResource ResourceExplorerColor}">
+                <StackPanel Orientation="Horizontal" Margin="5, 2, 2, 2">
+                  <Image VerticalAlignment="Center" Height="32" Width="32" Source="{Binding $parent[views:ClientView].((vm:ClientViewModel)DataContext).Client.CurrentUser.Avatar }" />
+                  <TextBlock Margin="6,0,0,0" FontSize="14" VerticalAlignment="Center" Text="{Binding $parent[views:ClientView].((vm:ClientViewModel)DataContext).Client.CurrentUser.Username }" />
+                </StackPanel>
+
+                <Button x:Name="PART_MenuButton" IsVisible="False" />
+                <Button x:Name="PART_PinButton" IsVisible="False" />
+                <Button x:Name="PART_MaximizeRestoreButton" IsVisible="False" />
+                <Button x:Name="PART_CloseButton" IsVisible="False" />
+              </Grid>
+            </Border>
+            <ContentPresenter x:Name="PART_ContentPresenter"
+                              BorderThickness="0, 0, 0, 0"
+                              Content="{TemplateBinding Content}"
+                              Grid.Row="1" />
+          </Grid>
+        </ControlTemplate>
+      </Setter>
+    </Style>
+  </UserControl.Styles>
+  
   <Grid RowDefinitions="Auto,*,20" ColumnDefinitions="Auto,*,40" Background="Transparent">
     <Border 
       BorderThickness="0,0,0,1"
diff --git a/Lightquark.NET/Client.cs b/Lightquark.NET/Client.cs
index 19ddf4fd0a22d5e2c414366e3d4203ae53eb2839..e0d22cfda5d521f9b50e95f2a8a42c3b90e0998b 100644
--- a/Lightquark.NET/Client.cs
+++ b/Lightquark.NET/Client.cs
@@ -1,20 +1,44 @@
 using System.Collections.ObjectModel;
 using System.ComponentModel;
+using AutoMapper;
 using CommunityToolkit.Mvvm.ComponentModel;
 using Lightquark.NET.Objects;
+using Lightquark.NET.Util;
 using Lightquark.NET.Util.Converters;
+using MongoDB.Bson;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Serialization;
 
 namespace Lightquark.NET;
 
+public enum GatewayConnectState
+{
+    NotConnected,
+    Connecting,
+    Connected
+}
+
 public partial class Client : ObservableObject
 {
+
     private const string Version = "v4";
+    public ObservableCollection<ObjectId> QuarkList { get; set; } = [];
     public ObservableCollection<Quark> Quarks { get; set; } = [];
     public ObservableCollection<Channel> Channels { get; set; } = [];
     public ObservableCollection<Message> Messages { get; set; } = [];
+    public ObservableCollection<User> Users { get; set; } = [];
+
+    public User? CurrentUser
+    {
+        get => _currentUser;
+        set => SetProperty(ref _currentUser, value);
+    }
+
     public Action? OnLogOut;
+    public Func<string, string, Task>? OnRefresh;
+    public EventBus EventBus = new();
+    public GatewayConnectState GatewayStatus { get; set; } = GatewayConnectState.NotConnected;
+    
 
     private string? _accessToken;
     private string? AccessToken
@@ -37,6 +61,8 @@ public partial class Client : ObservableObject
 
     private EnrichedNetworkInformation? _networkInformation;
     private HttpClient _httpClient = new();
+    private User? _currentUser;
+    private IMapper _mapper;
 
     public Client()
     {
@@ -44,9 +70,14 @@ public partial class Client : ObservableObject
         {
             Formatting = Formatting.Indented,
             NullValueHandling = NullValueHandling.Ignore,
-            Converters = new List<JsonConverter> { new AttachmentCreationConverter() },
+            Converters = new List<JsonConverter> { new AttachmentCreationConverter(), new StatusCreationConverter() },
             ContractResolver = new CamelCasePropertyNamesContractResolver()
         };
+        var mapperConfig = new MapperConfiguration(cfg =>
+        {
+            cfg.CreateMap<User, User>().ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
+        });
+        _mapper = mapperConfig.CreateMapper();
     }
 
     
@@ -76,6 +107,11 @@ public partial class Client : ObservableObject
                 {
                     _refreshTimer = new Timer(AcquireToken, null, timeUntilRefresh, Timeout.InfiniteTimeSpan);
                 }
+
+                if (GatewayStatus == GatewayConnectState.NotConnected)
+                {
+                    InitialConnectGateway();
+                }
             }
         }
         Console.WriteLine($"Detected {e.PropertyName} change");
diff --git a/Lightquark.NET/ClientMethods/Gateway.cs b/Lightquark.NET/ClientMethods/Gateway.cs
new file mode 100644
index 0000000000000000000000000000000000000000..e9baf1bfdb1ebd46733777f268a0ae408824e612
--- /dev/null
+++ b/Lightquark.NET/ClientMethods/Gateway.cs
@@ -0,0 +1,172 @@
+using Lightquark.NET.Objects;
+using Lightquark.NET.Objects.Reply;
+using Newtonsoft.Json;
+using Websocket.Client;
+
+namespace Lightquark.NET;
+
+public partial class Client
+{
+    private WebsocketClient? _gateway;
+    private Timer? _heartbeatTimer;
+    private string? _gatekeeperSession;
+    public ManualResetEventSlim GatewayConnectionEvent = new(false);
+    
+    private async void InitialConnectGateway()
+    {
+        if (NetworkInformation?.Gateway == null)
+        {
+            await Console.Error.WriteLineAsync("Network gateway uri null");
+            return;
+        }
+        GatewayStatus = GatewayConnectState.Connecting;
+        _gateway = new WebsocketClient(new Uri(NetworkInformation.Gateway));
+        _gateway.ReconnectTimeout = null;
+        _gateway.DisconnectionHappened.Subscribe(info =>
+        {
+            GatewayConnectionEvent.Reset();
+            GatewayStatus = GatewayConnectState.NotConnected;
+            Console.Error.WriteLine($"Disconnection from gateway, type {info.Type}");
+        });
+        _gateway.ReconnectionHappened.Subscribe(info =>
+        {
+            Console.Error.WriteLine($"Reconnection to gateway, type {info.Type}");
+            SendGatewayMessage(new { @event = "authenticate", gatekeeperSession = _gatekeeperSession, token = AccessToken });
+        });
+        _gateway.MessageReceived.Subscribe(msg =>
+        {
+            try
+            {
+                var message = ParseGateway<BaseGatewayMessage>(msg.Text!);
+                if (message.Event != "heartbeat")
+                {
+                    Console.WriteLine(msg.Text);
+                }
+                EventBus.Publish(new GatewayBusEvent
+                {
+                    BaseMessage = message,
+                    RawMessageString = msg.Text!
+                });
+                switch (message.Event)
+                {
+                    case "authenticate":
+                        var authMessage = ParseGateway<AuthGatewayMessage>(msg.Text!);
+                        switch (authMessage.Code)
+                        {
+                            case 200:
+                            {
+                                GatewayStatus = GatewayConnectState.Connected;
+                                GatewayConnectionEvent.Set();
+                                Console.WriteLine("Connected to gateway!");
+                                if (_heartbeatTimer == null)
+                                {
+                                    _heartbeatTimer = new Timer(Heartbeat, null, TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(15));
+                                }
+
+                                break;
+                            }
+                            case 401:
+                                LogOut();
+                                break;
+                            default:
+                                Console.Error.WriteLine($"Auth failure to gateway! Code {authMessage.Code}");
+                                break;
+                        }
+                        break;
+                    case "rpc":
+                        break;
+                    case "userUpdate":
+                        var userUpdateMessage = ParseGateway<UserUpdateGatewayMessage>(msg.Text!);
+                        if (userUpdateMessage.User.Id == CurrentUser?.Id)
+                        {
+                            CurrentUserFromObject(userUpdateMessage.User);
+                        }
+
+                        break;
+                }
+            }
+            catch (Exception ex)
+            {
+                Console.Error.WriteLine(ex);
+            }
+        });
+
+        await _gateway.Start();
+    }
+
+    private void Heartbeat(object? state)
+    {
+        SendGatewayMessage(new {@event = "heartbeat"});
+    }
+
+    public async Task<Reply<T>> CallRpc<T>(string method, string route, object? body) where T : BaseReplyResponse
+    {
+        if (GatewayStatus != GatewayConnectState.Connected) throw new Exception("RPC Call when Gateway not connected!");
+        var sem = new SemaphoreSlim(0, 1);
+        var state = Guid.NewGuid().ToString();
+        RpcGatewayMessage<T>? responseBody = null;
+        var subscriptionId = EventBus.Subscribe<GatewayBusEvent>(busEvent =>
+        {
+            if (busEvent.BaseMessage.State != state) return;
+            responseBody = ParseGateway<RpcGatewayMessage<T>>(busEvent.RawMessageString);
+            sem.Release();
+        });
+        SendGatewayMessage(new
+        {
+            @event = "rpc",
+            token = AccessToken,
+            state,
+            route,
+            method,
+            body
+        });
+        await sem.WaitAsync();
+        EventBus.Unsubscribe<GatewayBusEvent>(subscriptionId);
+        
+        return responseBody!.Body;
+    }
+    
+    private void SendGatewayMessage(object message)
+    {
+        _gateway.Send(JsonConvert.SerializeObject(message));
+    }
+
+    private T ParseGateway<T>(string message)
+    {
+        return JsonConvert.DeserializeObject<T>(message)!;
+    }
+}
+
+public record BaseGatewayMessage
+{
+    [JsonProperty("event")]
+    public required string Event { get; init; }
+    
+    [JsonProperty("state")]
+    public string? State { get; init; }
+}
+
+
+public record AuthGatewayMessage : BaseGatewayMessage
+{
+    [JsonProperty("code")]
+    public required int Code { get; init; }
+}
+
+public record UserUpdateGatewayMessage : BaseGatewayMessage
+{
+    [JsonProperty("user")]
+    public required User User { get; init; }
+}
+
+public record GatewayBusEvent
+{
+    public required BaseGatewayMessage BaseMessage { get; init; }
+    public required string RawMessageString { get; init; }
+}
+
+public record RpcGatewayMessage<T> : BaseGatewayMessage where T : BaseReplyResponse
+{
+    [JsonProperty("body")]
+    public required Reply<T> Body { get; init; }
+}
\ No newline at end of file
diff --git a/Lightquark.NET/ClientMethods/Login.cs b/Lightquark.NET/ClientMethods/Login.cs
index d0be610c2b5ea2bc23b227fde097a3011a38ca0b..3bab6b27ec1af79ee143c5efc2d5e6215e0fa7c8 100644
--- a/Lightquark.NET/ClientMethods/Login.cs
+++ b/Lightquark.NET/ClientMethods/Login.cs
@@ -63,26 +63,30 @@ public partial class Client
             {
                 accessToken = AccessToken, refreshToken = RefreshToken
             }), new MediaTypeHeaderValue("application/json")));
-            var parsedRes = JsonConvert.DeserializeObject<Reply<RefreshReplyResponse>>(await res.Content.ReadAsStringAsync());
+            var parsedRes =
+                JsonConvert.DeserializeObject<Reply<RefreshReplyResponse>>(await res.Content.ReadAsStringAsync());
 
             if (parsedRes == null)
             {
                 throw new Exception("Parsing failure");
             }
-            
+
             switch (res.StatusCode)
             {
                 case HttpStatusCode.OK:
                     AccessToken = parsedRes.Response.AccessToken;
                     Console.WriteLine("Token refreshed! Happy days.");
+                    var refreshHandlerTask = OnRefresh?.Invoke(parsedRes.Response.AccessToken!, RefreshToken!);
+                    if (refreshHandlerTask != null) await refreshHandlerTask;
                     break;
                 default:
-                    await Console.Error.WriteLineAsync($"Failed to refresh ({res.StatusCode}) ({parsedRes?.Response?.Message})");
+                    await Console.Error.WriteLineAsync(
+                        $"Failed to refresh ({res.StatusCode}) ({parsedRes?.Response?.Message})");
                     AccessToken = null;
                     RefreshToken = null;
                     break;
             }
-            
+
         }
         catch (Exception ex)
         {
@@ -93,8 +97,10 @@ public partial class Client
 
     public void LogOut()
     {
+        Console.WriteLine("LogOut");
         AccessToken = null;
         RefreshToken = null;
+        _gateway.Dispose();
     }
 
     public void RunLogOut()
diff --git a/Lightquark.NET/ClientMethods/User.cs b/Lightquark.NET/ClientMethods/User.cs
new file mode 100644
index 0000000000000000000000000000000000000000..4c32f1b463b519b0910f1c392f5ec342ffd888cb
--- /dev/null
+++ b/Lightquark.NET/ClientMethods/User.cs
@@ -0,0 +1,31 @@
+using Avalonia.Media.Imaging;
+using Lightquark.NET.Objects;
+using Lightquark.NET.Objects.Reply;
+
+namespace Lightquark.NET;
+
+public partial class Client
+{
+    public async Task GetCurrentUser()
+    {
+        var res = await CallRpc<UserReplyResponse>("GET", "/v4/user/me", null);
+        if (!res.Request.Success) throw new Exception("Failed to fetch user");
+        CurrentUserFromObject(res.Response.User!);
+    }
+
+    private async void CurrentUserFromObject(User user)
+    {
+        try
+        {
+            var avatarRes = await _httpClient.GetAsync(user!.AvatarUriGetter);
+            user.Avatar = new Bitmap(await avatarRes.Content.ReadAsStreamAsync());
+        }
+        catch (Exception ex)
+        {
+            Console.Error.WriteLine(ex);
+        }
+        CurrentUser = _mapper.Map(user, CurrentUser);
+        Console.WriteLine(CurrentUser.Status.PrimaryText);
+        Console.WriteLine(CurrentUser.AvatarUriGetter);
+    }
+}
\ No newline at end of file
diff --git a/Lightquark.NET/Lightquark.NET.csproj b/Lightquark.NET/Lightquark.NET.csproj
index 4463863201054c4605436c925058df1a07d18d84..2cb7d5bd3070f714c6e970c08cef75f5e60b5c38 100644
--- a/Lightquark.NET/Lightquark.NET.csproj
+++ b/Lightquark.NET/Lightquark.NET.csproj
@@ -7,14 +7,12 @@
     </PropertyGroup>
 
     <ItemGroup>
+      <PackageReference Include="AutoMapper" Version="13.0.1" />
       <PackageReference Include="Avalonia" Version="11.1.3" />
       <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
       <PackageReference Include="Lightquark.Types" Version="2024.9.15.139" />
       <PackageReference Include="Markdig" Version="0.37.0" />
-    </ItemGroup>
-
-    <ItemGroup>
-      <Folder Include="Util\" />
+      <PackageReference Include="Websocket.Client" Version="5.1.2" />
     </ItemGroup>
 
 </Project>
diff --git a/Lightquark.NET/Objects/Reply/UserReply.cs b/Lightquark.NET/Objects/Reply/UserReply.cs
new file mode 100644
index 0000000000000000000000000000000000000000..d2c3cd560610db2ac9e5e9a58f13dba7bf020347
--- /dev/null
+++ b/Lightquark.NET/Objects/Reply/UserReply.cs
@@ -0,0 +1,9 @@
+using Newtonsoft.Json;
+
+namespace Lightquark.NET.Objects.Reply;
+
+public record UserReplyResponse : BaseReplyResponse
+{
+    [JsonProperty("user")] 
+    public User? User;
+}
\ No newline at end of file
diff --git a/Lightquark.NET/Objects/Status.cs b/Lightquark.NET/Objects/Status.cs
index 642938c49e3960aa5b8160b1764ba954a3384b75..90eb5b7369024a88d00e8d408b4b31c886ba1ba4 100644
--- a/Lightquark.NET/Objects/Status.cs
+++ b/Lightquark.NET/Objects/Status.cs
@@ -10,15 +10,15 @@ public class Status : IStatus
 {
     [JsonProperty("_id")]
     [JsonConverter(typeof(ObjectIdConverter))]
-    public required ObjectId Id { get; set; }
+    public ObjectId Id { get; set; }
     
     [JsonIgnore]
-    public required ObjectId UserId { get; set; }
+    public ObjectId UserId { get; set; }
     
     [JsonConverter(typeof(LowerCaseStringEnumConverter))]
-    public required StatusType Type { get; set; }
+    public StatusType Type { get; set; }
    
-    public required string PrimaryText { get; set; }
+    public string PrimaryText { get; set; }
     
     public string? Text1 { get; set; }
     
diff --git a/Lightquark.NET/Objects/User.cs b/Lightquark.NET/Objects/User.cs
index 70ab097bd2a1b0b64f7976aa44eb4a2bcc8151b5..968d7bc34e6c31d78ddd12420cab8335d3279d72 100644
--- a/Lightquark.NET/Objects/User.cs
+++ b/Lightquark.NET/Objects/User.cs
@@ -1,53 +1,103 @@
-using Lightquark.NET.Util.Converters;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Media.Imaging;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Lightquark.NET.Util.Converters;
 using Lightquark.Types.Mongo;
 using MongoDB.Bson;
 using Newtonsoft.Json;
 
 namespace Lightquark.NET.Objects;
 
-public class User : IUser
+public class User : ObservableObject, IUser 
 {
+    private Bitmap _avatar;
+    private IStatus? _status;
+    private string? _pronouns;
+    private ObjectId _id;
+    private string _email;
+    private string _username;
+    private bool _admin;
+    private bool _isBot;
+    private bool _secretThirdThing;
+    private Uri _avatarUriGetter;
+
     [JsonProperty("_id")]
     [JsonConverter(typeof(ObjectIdConverter))]
-    public ObjectId Id { get; set; }
+    public ObjectId Id
+    {
+        get => _id;
+        set => SetProperty(ref _id, value);
+    }
 
     [JsonIgnore]
     public required byte[] PasswordHash { get; init; }
-    
+
     [JsonProperty("email")]
-    public required string Email { get; set; }
-    
+    public required string Email
+    {
+        get => _email;
+        [MemberNotNull(nameof(_email))] set => SetProperty(ref _email, value);
+    }
+
     [JsonIgnore]
     public required byte[] Salt { get; init; }
-    
+
     [JsonProperty("username")]
-    public required string Username { get; set; }
-    
+    public required string Username
+    {
+        get => _username;
+        [MemberNotNull(nameof(_username))] set => SetProperty(ref _username, value);
+    }
+
     [JsonProperty("admin")]
-    public bool Admin { get; set; }
-    
+    public bool Admin
+    {
+        get => _admin;
+        set => SetProperty(ref _admin, value);
+    }
+
     [JsonProperty("isBot")]
-    public bool IsBot { get; set; }
-    
+    public bool IsBot
+    {
+        get => _isBot;
+        set => SetProperty(ref _isBot, value);
+    }
+
     [JsonProperty("secretThirdThing")]
-    public bool SecretThirdThing { get; set; }
-    
-    [JsonProperty("pronouns")]
-    public string? Pronouns { get; set; }
+    public bool SecretThirdThing
+    {
+        get => _secretThirdThing;
+        set => SetProperty(ref _secretThirdThing, value);
+    }
 
+    [JsonProperty("pronouns")]
+    public string? Pronouns
+    {
+        get => _pronouns;
+        set => SetProperty(ref _pronouns, value);
+    }
 
-    // Virtual fields
-    [JsonIgnore]
-    public Status[]? VirtualStatuses { get; set; } // Store multiple statuses
+    [JsonProperty("status")]
+    public IStatus? Status
+    {
+        get => _status;
+        set => SetProperty(ref _status, value);
+    }
 
-    public IStatus? Status => VirtualStatuses?.FirstOrDefault();
+    public Bitmap Avatar
+    {
+        get => _avatar;
+        set => SetProperty(ref _avatar, value);
+    }
 
     // Calculated field
-    [JsonIgnore]
-    public Uri? AvatarUri { get; set; }
+    [JsonProperty("avatarUri")]
+    public Uri AvatarUriGetter
+    {
+        get => _avatarUriGetter;
+        set => SetProperty(ref _avatarUriGetter, value);
+    }
 
-    [JsonProperty("avatarUri")] public Uri AvatarUriGetter => AvatarUri!;
-    
     [JsonIgnore]
     [JsonConverter(typeof(ObjectIdConverter))]
     public ObjectId? AvatarFileId { get; set; }
diff --git a/Lightquark.NET/Util/Converters/StatusCreationConverter.cs b/Lightquark.NET/Util/Converters/StatusCreationConverter.cs
new file mode 100644
index 0000000000000000000000000000000000000000..bb02ad89f10cc3edd6ca9dd5380383505dc3ded0
--- /dev/null
+++ b/Lightquark.NET/Util/Converters/StatusCreationConverter.cs
@@ -0,0 +1,13 @@
+using Lightquark.NET.Objects;
+using Lightquark.Types.Mongo;
+using Newtonsoft.Json.Converters;
+
+namespace Lightquark.NET.Util.Converters;
+
+public class StatusCreationConverter : CustomCreationConverter<IStatus>
+{
+    public override IStatus Create(Type objectType)
+    {
+        return new Status();
+    }
+}
\ No newline at end of file
diff --git a/Lightquark.NET/Util/EventBus.cs b/Lightquark.NET/Util/EventBus.cs
new file mode 100644
index 0000000000000000000000000000000000000000..5f96b2acd36492f1c21f45c84e9e3971183ac66a
--- /dev/null
+++ b/Lightquark.NET/Util/EventBus.cs
@@ -0,0 +1,52 @@
+namespace Lightquark.NET.Util;
+
+public class EventBus
+{
+    private readonly Dictionary<Type, List<EventBusSubscription>> _subscriptions = new();
+
+    public void Publish<TEvent>(TEvent @event)
+    {
+        var eventType = typeof(TEvent);
+        if (@event == null || !_subscriptions.TryGetValue(eventType, out var handlers)) return;
+        Task.Run(() =>
+        {
+            foreach (var subscription in handlers)
+            {
+                subscription.Handler(@event);
+            }
+        });
+    }
+
+    public Guid Subscribe<TEvent>(Action<TEvent> handler)
+    {
+        var eventType = typeof(TEvent);
+        if (!_subscriptions.TryGetValue(eventType, out var value))
+        {
+            value = [];
+            _subscriptions[eventType] = value;
+        }
+
+        var subscription = new EventBusSubscription
+        {
+            Handler = e => handler((TEvent)e),
+            SubscriptionId = Guid.NewGuid()
+        };
+        value.Add(subscription);
+        return subscription.SubscriptionId;
+    }
+
+    public void Unsubscribe<TEvent>(Guid subscriptionId)
+    {
+        var eventType = typeof(TEvent);
+        if (_subscriptions.TryGetValue(eventType, out var value))
+        {
+            value.RemoveAll(s => s.SubscriptionId == subscriptionId);
+        }
+    }
+}
+
+public record EventBusSubscription
+{
+    public required Action<object> Handler;
+    public required Guid SubscriptionId;
+}
\ No newline at end of file