diff --git a/backend/lib/authenticator.dart b/backend/lib/authenticator.dart
index aafba32..46db183 100644
--- a/backend/lib/authenticator.dart
+++ b/backend/lib/authenticator.dart
@@ -24,6 +24,7 @@ class Authenticator {
 
     final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000;
     final jwt = JWT(
+      header: {'algo': 'HS256'},
       JWTBody(uuid: newUser.uuid, roomUuid: newUser.gameRoomUuid, iat: iat, exp: iat + expTimeSecs).toJson(),
     );
 
diff --git a/backend/lib/utils/environment.dart b/backend/lib/utils/environment.dart
index ee2f896..477f20c 100644
--- a/backend/lib/utils/environment.dart
+++ b/backend/lib/utils/environment.dart
@@ -8,7 +8,7 @@ final log = Logger('Environment');
 
 bool _isDevEnv = false;
 
-void checkEnvironment(bool isDevEnv) {
+void checkEnvironment({required bool isDevEnv}) {
   _isDevEnv = isDevEnv;
   getJWTSecret();
 }
@@ -46,7 +46,9 @@ String? getJWTSecret() {
     if (_isDevEnv) {
       log.warning('JWT secret not configured. Define JWT_TOKEN_SECRET in environment.');
       final secret = List.generate(
-          24, (_) => String.fromCharCode((65 + Random().nextInt(26)) + (Random().nextInt(2) == 0 ? 0 : 32))).join();
+        24,
+        (_) => String.fromCharCode((65 + Random().nextInt(26)) + (Random().nextInt(2) == 0 ? 0 : 32)),
+      ).join();
       log.warning('Generated random JWT secret for development: $secret');
       return secret;
     } else {
diff --git a/backend/main.dart b/backend/main.dart
index c69d4c6..b59fca5 100644
--- a/backend/main.dart
+++ b/backend/main.dart
@@ -3,7 +3,7 @@ import 'dart:io';
 
 import 'package:backend/utils/environment.dart';
 import 'package:dart_frog/dart_frog.dart';
-import 'package:logging/logging.dart';
+import 'package:shared_models/fart_logger.dart';
 
 bool _listening = false;
 
@@ -12,54 +12,11 @@ Future<HttpServer> run(Handler handler, InternetAddress ip, int port) async {
   // Logic to prevent multiple listeners with hot-reload
   // Changes to this are not hot-reloaded, you must restart the server
   if (!_listening) {
-    final logLevel = Platform.environment['LOG_LEVEL'] ?? (isDevelopment ? 'FINEST' : 'INFO');
-    Logger.root.level =
-        Level.LEVELS.firstWhere((l) => l.name == logLevel, orElse: () => Level.INFO); // defaults to Level.INFO
-    Logger.root.onRecord.listen((record) {
-      writeLogRecord(record, record.level.value >= Level.SEVERE.value ? stderr : stdout);
-    });
+    FartLogger.listen(isDevelopment: isDevelopment);
     _listening = true;
   }
 
-  checkEnvironment(isDevelopment);
-
-  for (final lvl in Level.LEVELS) {
-    writeLogRecord(LogRecord(lvl, 'Test message', 'main'), stdout);
-  }
+  checkEnvironment(isDevEnv: isDevelopment);
 
   return serve(handler, ip, port);
 }
-
-const Map<String, String> _levelColors = {
-  'FINEST': '\x1B[1;37m', // White
-  'FINER': '\x1B[1;38m', // Gray
-  'FINE': '\x1B[1;35m', // Purple
-  'CONFIG': '\x1B[1;36m', // Cyan
-  'INFO': '\x1B[1;32m', // Green
-  'WARNING': '\x1B[1;33m', // Yellow
-  'SEVERE': '\x1B[1;31m', // Red
-  'SHOUT': '\x1B[1;38;5;52m\x1B[1;48;5;213m', // Red on pink
-};
-
-const String _resetColor = '\x1B[0m';
-
-String _getColoredLevel(String levelName) {
-  return '${_levelColors[levelName] ?? ''}$levelName$_resetColor';
-}
-
-void writeLogRecord(LogRecord record, IOSink iosink) {
-  // Write the basic log message with colored level
-  iosink.writeln(
-    '[${_getColoredLevel(record.level.name)}]:[${record.loggerName}] '
-    '${record.time}: ${record.message}',
-  );
-
-  // Additional details for severe logs
-  if (record.level.value >= Level.SEVERE.value) {
-    iosink.writeln(
-      '[${_getColoredLevel(record.level.name)}]:[${record.loggerName}] '
-      '${record.error?.toString() ?? "No error provided"}\n'
-      '${record.stackTrace?.toString() ?? "No trace provided"}',
-    );
-  }
-}
diff --git a/backend/routes/join_room.dart b/backend/routes/join_room.dart
index d55ddf1..418df40 100644
--- a/backend/routes/join_room.dart
+++ b/backend/routes/join_room.dart
@@ -15,8 +15,8 @@ Future<Response> onRequest(RequestContext context) async {
 
   try {
     // Parse the request body
-    final body = await context.request.json() as Map<String, dynamic>;
-    final createUserReq = CreateUserRequest.fromJson(body);
+    final body = await context.request.json();
+    final createUserReq = CreateUserRequest.fromJson(body as Map<String, dynamic>);
 
     // Generate token
     final authenticator = context.read<Authenticator>();
diff --git a/backend/scripts/pubspec.yaml b/backend/scripts/pubspec.yaml
index 9fb8a2d..a4fa2db 100644
--- a/backend/scripts/pubspec.yaml
+++ b/backend/scripts/pubspec.yaml
@@ -3,6 +3,6 @@ environment:
   sdk: ">=3.0.0 <4.0.0"
 
 dev_dependencies:
-  test: ^1.24.0
   http: ^1.1.0
+  test: ^1.24.0
 
diff --git a/backend/scripts/test_runner.dart b/backend/scripts/test_runner.dart
index 81ac548..6d1389d 100644
--- a/backend/scripts/test_runner.dart
+++ b/backend/scripts/test_runner.dart
@@ -61,7 +61,7 @@ void main() async {
       final sub = serverLogs.length > 10 ? serverLogs.length - 10 : 0;
       stdout.write("Server logs:\n${serverLogs.sublist(sub).join('\n')}");
     } else {
-      stdout.writeln("💨 Passed like a light breeze 😮‍💨");
+      stdout.writeln('💨 Passed like a light breeze 😮‍💨');
     }
 
     // Exit with the same code as the test process
diff --git a/backend/test_e2e/pubspec.yaml b/backend/test_e2e/pubspec.yaml
index cb25b75..908e86f 100644
--- a/backend/test_e2e/pubspec.yaml
+++ b/backend/test_e2e/pubspec.yaml
@@ -7,7 +7,7 @@ environment:
 dependencies:
   backend:
     path: ..
+  http: ^1.1.0
   shared_models:
     path: ../../shared_models
   test: ^1.24.0
-  http: ^1.1.0
diff --git a/frontend/lib/features/room/game_room.dart b/frontend/lib/features/room/game_room.dart
new file mode 100644
index 0000000..53d98d5
--- /dev/null
+++ b/frontend/lib/features/room/game_room.dart
@@ -0,0 +1,42 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:frontend/providers/auth.dart';
+import 'package:go_router/go_router.dart';
+import 'package:logging/logging.dart';
+
+final logger = Logger('GameRoomHome');
+
+class GameRoomHome extends ConsumerStatefulWidget {
+  const GameRoomHome({super.key, this.roomUuid});
+
+  final String? roomUuid;
+
+  @override
+  ConsumerState<GameRoomHome> createState() => _GameRoomHomeState();
+}
+
+class _GameRoomHomeState extends ConsumerState<GameRoomHome> {
+  @override
+  Widget build(BuildContext context) {
+    final jwtAsync = ref.watch(jwtNotifierProvider);
+    return Scaffold(
+      body: jwtAsync.when(
+        data: (jwt) {
+          if (jwt == null || jwt.roomUuid != widget.roomUuid) {
+            logger.fine('Tried to open room, but not authenticated / wrong room');
+            // return home
+            context.go('/');
+          }
+          return Column(
+            children: [
+              Text('Authenticated.'),
+              Text('Welcome to room ${widget.roomUuid}'),
+            ],
+          );
+        },
+        loading: () => CircularProgressIndicator(),
+        error: (e, st) => Text('$e, $st'),
+      ),
+    );
+  }
+}
diff --git a/frontend/lib/features/room/join_room.dart b/frontend/lib/features/room/join_room.dart
new file mode 100644
index 0000000..43883dd
--- /dev/null
+++ b/frontend/lib/features/room/join_room.dart
@@ -0,0 +1,167 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:frontend/features/room/service/game_room.dart';
+import 'package:frontend/providers/auth.dart';
+import 'package:go_router/go_router.dart';
+import 'package:logging/logging.dart';
+
+final logger = Logger('JoinRoomHome');
+
+class JoinRoomHome extends ConsumerStatefulWidget {
+  const JoinRoomHome({super.key});
+
+  @override
+  ConsumerState<JoinRoomHome> createState() => _JoinRoomHomeState();
+}
+
+class _JoinRoomHomeState extends ConsumerState<JoinRoomHome> {
+  final _formKey = GlobalKey<FormState>();
+  final _codeController = TextEditingController();
+  final _nameController = TextEditingController();
+  bool _isLoading = false;
+
+  @override
+  void dispose() {
+    _codeController.dispose();
+    _nameController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final jwtAsync = ref.watch(jwtNotifierProvider);
+
+    jwtAsync.whenData((jwt) {
+      logger.fine('Got jwt: ${jwt == null ? 'NULL' : jwt.toString().substring(10)}');
+      if (jwt == null) return;
+      logger.fine('Navigating to game room screen');
+      WidgetsBinding.instance.addPostFrameCallback(
+        (_) => context.go('/room/${jwt.roomUuid}'),
+      );
+    });
+
+    return Scaffold(
+      body: Padding(
+        padding: const EdgeInsets.all(16.0),
+        child: Form(
+          key: _formKey,
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              TextFormField(
+                controller: _codeController,
+                textCapitalization: TextCapitalization.characters,
+                style: const TextStyle(
+                  letterSpacing: 1.5,
+                  fontWeight: FontWeight.bold,
+                ),
+                decoration: const InputDecoration(
+                  labelText: 'Enter Room Code',
+                  hintText: 'ABCDEF',
+                  helperText: 'Enter 6 uppercase letters',
+                  border: OutlineInputBorder(),
+                  errorStyle: TextStyle(height: 0.8),
+                  counterText: '', // Hides the built-in counter
+                ),
+                maxLength: 6,
+                textInputAction: TextInputAction.done,
+                inputFormatters: [
+                  // FilteringTextInputFormatter.allow(RegExp('[A-Z]')),
+                  UpperCaseTextFormatter(),
+                ],
+                validator: (value) {
+                  if (value == null || value.isEmpty) {
+                    return 'Room code is required';
+                  }
+                  if (value.length != 6) {
+                    return 'Code must be exactly 6 characters';
+                  }
+                  if (!RegExp(r'^[A-Z]{6}$').hasMatch(value)) {
+                    return 'Only uppercase letters are allowed';
+                  }
+                  return null;
+                },
+                onChanged: (value) {
+                  // Convert to uppercase while typing
+                  if (value != value.toUpperCase()) {
+                    _codeController.text = value.toUpperCase();
+                    _codeController.selection = TextSelection.fromPosition(
+                      TextPosition(offset: _codeController.text.length),
+                    );
+                  }
+                },
+              ),
+              const SizedBox(height: 16),
+              TextFormField(
+                controller: _nameController,
+                decoration: const InputDecoration(
+                  labelText: 'Name',
+                  border: OutlineInputBorder(),
+                ),
+                keyboardType: TextInputType.text,
+                maxLength: 20,
+                validator: (value) {
+                  if (value == null || value.isEmpty) {
+                    return 'Please enter a name ya goof';
+                  }
+                  return null;
+                },
+              ),
+              const SizedBox(height: 16),
+              ElevatedButton(
+                onPressed: _isLoading
+                    ? null
+                    : () async {
+                        if (_formKey.currentState!.validate()) {
+                          setState(() => _isLoading = true);
+                          try {
+                            ref.read(
+                              joinRoomProvider(
+                                username: _nameController.text,
+                                code: _codeController.text,
+                              ),
+                            );
+                            // )
+                            //     .whenData(
+                            //   (response) {
+                            //     if (response != null && response.uuid != null) {
+                            //       logger.fine('Navigating to room ${response.uuid}');
+                            //       // context.go('room/${response.uuid}');
+                            //     } else {
+                            //       ScaffoldMessenger.of(context).showSnackBar(
+                            //         SnackBar(
+                            //           content: Text('Unexpected error occurred.'),
+                            //           backgroundColor: Colors.red,
+                            //         ),
+                            //       );
+                            //     }
+                            //   },
+                            // );
+                          } finally {
+                            setState(() => _isLoading = false);
+                          }
+                        }
+                      },
+                child: _isLoading ? const CircularProgressIndicator() : const Text('Join Room'),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class UpperCaseTextFormatter extends TextInputFormatter {
+  @override
+  TextEditingValue formatEditUpdate(
+    TextEditingValue oldValue,
+    TextEditingValue newValue,
+  ) {
+    return TextEditingValue(
+      text: newValue.text.toUpperCase(),
+      selection: newValue.selection,
+    );
+  }
+}
diff --git a/frontend/lib/features/room/service/game_room.dart b/frontend/lib/features/room/service/game_room.dart
new file mode 100644
index 0000000..7e010af
--- /dev/null
+++ b/frontend/lib/features/room/service/game_room.dart
@@ -0,0 +1,37 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:frontend/providers/auth.dart';
+import 'package:frontend/providers/dio.dart';
+import 'package:logging/logging.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'package:shared_models/user.dart';
+
+part 'game_room.g.dart';
+
+final logger = Logger('services/joinRoom');
+
+@riverpod
+Future<JoinRoomResponse?> joinRoom(Ref ref, {required String username, required String code}) async {
+  final dio = ref.read(dioProvider);
+
+  try {
+    final response = await dio.post<Map<String, dynamic>>(
+      '/join_room',
+      data: JoinRoomRequest(username: username, roomCode: code).toJson(),
+    );
+
+    if (response.statusCode == 200 && response.data != null) {
+      final joinResponse = JoinRoomResponse.fromJson(response.data!);
+      if (joinResponse.token != null) {
+        logger.fine('Setting token: ${joinResponse.token!.substring(10)}');
+        await ref.read(jwtNotifierProvider.notifier).setJwt(joinResponse.token!);
+      }
+      return joinResponse;
+    } else {
+      logger.warning('Could not join room');
+    }
+  } catch (e) {
+    logger.severe('Failed to join room', e, StackTrace.current);
+    return null;
+  }
+  return null;
+}
diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart
index 2830484..ef0cbeb 100644
--- a/frontend/lib/main.dart
+++ b/frontend/lib/main.dart
@@ -1,7 +1,17 @@
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:frontend/features/room/join_room.dart';
+import 'package:go_router/go_router.dart';
+import 'package:shared_models/fart_logger.dart';
+
+import 'features/room/game_room.dart';
 
 void main() {
-  runApp(const MyApp());
+  // determine if flutter app is dev or prod env
+  FartLogger.listen(isDevelopment: kDebugMode);
+
+  runApp(ProviderScope(child: const MyApp()));
 }
 
 class MyApp extends StatelessWidget {
@@ -9,87 +19,27 @@ class MyApp extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return MaterialApp(
-      title: 'Flutter Demo',
+    final router = buildAppRouter();
+
+    return MaterialApp.router(
+      title: 'FartStack Demo',
       theme: ThemeData(
         colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
         useMaterial3: true,
       ),
-      home: const MyHomePage(title: 'Flutter Demo Home Page'),
+      routerConfig: router,
     );
   }
 }
 
-class MyHomePage extends StatefulWidget {
-  const MyHomePage({super.key, required this.title});
-
-  final String title;
-
-  @override
-  State<MyHomePage> createState() => _MyHomePageState();
-}
-
-class _MyHomePageState extends State<MyHomePage> {
-  int _counter = 0;
-
-  void _incrementCounter() {
-    setState(() {
-      _counter++;
-    });
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    // This method is rerun every time setState is called, for instance as done
-    // by the _incrementCounter method above.
-    //
-    // The Flutter framework has been optimized to make rerunning build methods
-    // fast, so that you can just rebuild anything that needs updating rather
-    // than having to individually change instances of widgets.
-    return Scaffold(
-      appBar: AppBar(
-        // TRY THIS: Try changing the color here to a specific color (to
-        // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
-        // change color while the other colors stay the same.
-        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
-        // Here we take the value from the MyHomePage object that was created by
-        // the App.build method, and use it to set our appbar title.
-        title: Text(widget.title),
+GoRouter buildAppRouter() {
+  return GoRouter(routes: [
+    GoRoute(path: '/', builder: (ctx, state) => JoinRoomHome()),
+    GoRoute(
+      path: '/room/:roomUuid',
+      builder: (ctx, state) => GameRoomHome(
+        roomUuid: state.pathParameters['roomUuid'],
       ),
-      body: Center(
-        // Center is a layout widget. It takes a single child and positions it
-        // in the middle of the parent.
-        child: Column(
-          // Column is also a layout widget. It takes a list of children and
-          // arranges them vertically. By default, it sizes itself to fit its
-          // children horizontally, and tries to be as tall as its parent.
-          //
-          // Column has various properties to control how it sizes itself and
-          // how it positions its children. Here we use mainAxisAlignment to
-          // center the children vertically; the main axis here is the vertical
-          // axis because Columns are vertical (the cross axis would be
-          // horizontal).
-          //
-          // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
-          // action in the IDE, or press "p" in the console), to see the
-          // wireframe for each widget.
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: <Widget>[
-            const Text(
-              'You have pushed the button this many times:',
-            ),
-            Text(
-              '$_counter',
-              style: Theme.of(context).textTheme.headlineMedium,
-            ),
-          ],
-        ),
-      ),
-      floatingActionButton: FloatingActionButton(
-        onPressed: _incrementCounter,
-        tooltip: 'Increment',
-        child: const Icon(Icons.add),
-      ), // This trailing comma makes auto-formatting nicer for build methods.
-    );
-  }
+    ),
+  ]);
 }
diff --git a/frontend/lib/providers/auth.dart b/frontend/lib/providers/auth.dart
index d6c1967..1646e35 100644
--- a/frontend/lib/providers/auth.dart
+++ b/frontend/lib/providers/auth.dart
@@ -1,12 +1,60 @@
+import 'package:jwt_decoder/jwt_decoder.dart';
+import 'package:logging/logging.dart';
 import 'package:riverpod_annotation/riverpod_annotation.dart';
 import 'package:shared_models/jwt.dart';
+import 'package:shared_preferences/shared_preferences.dart';
 
 part 'auth.g.dart';
 
+final logger = Logger('provider/auth');
+
 @riverpod
 class JwtNotifier extends _$JwtNotifier {
   @override
-  JWTBody? build() {
-    return null;
+  Future<JWTBody?> build() async {
+    if (!await SharedPreferencesAsync().containsKey('jwt')) {
+      logger.fine('No JWT saved to client');
+      return null;
+    }
+    final jwtString = await SharedPreferencesAsync().getString('jwt');
+    if (jwtString == null) {
+      logger.fine('Saved JWT came back null, removing key');
+      SharedPreferencesAsync().remove('jwt');
+      return null;
+    }
+
+    final payload = JwtDecoder.tryDecode(jwtString);
+    if (payload == null) {
+      logger.fine('Failed to decode JWT, removing key.');
+      SharedPreferencesAsync().remove('jwt');
+      return null;
+    }
+
+    try {
+      final body = JWTBody.fromJson(payload);
+      return body;
+    } catch (e) {
+      logger.shout('Failed to parse JWT payload to JWTBody, something is wrong:', e, StackTrace.current);
+      return null;
+    }
+  }
+
+  Future<void> setJwt(String jwt) async {
+    final payload = JwtDecoder.tryDecode(jwt);
+    if (payload == null) {
+      logger.info('Tried to set JWT token that did not decode to payload');
+      state = AsyncValue.error('JWT set to invalid token', StackTrace.current);
+      return;
+    }
+
+    logger.fine('Saving jwt token to shared prefs');
+    await SharedPreferencesAsync().setString('jwt', jwt);
+
+    try {
+      final jwtBody = JWTBody.fromJson(payload);
+      state = AsyncValue.data(jwtBody);
+    } catch (e) {
+      state = AsyncError(e, StackTrace.current);
+    }
   }
 }
diff --git a/frontend/lib/providers/dio.dart b/frontend/lib/providers/dio.dart
new file mode 100644
index 0000000..edd4dc4
--- /dev/null
+++ b/frontend/lib/providers/dio.dart
@@ -0,0 +1,47 @@
+import 'package:dio/dio.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:logging/logging.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'dio.g.dart';
+
+final logger = Logger('Dio');
+
+@riverpod
+Dio dio(Ref ref) {
+  final dio = Dio(BaseOptions(
+    baseUrl: 'http://localhost:8080',
+    connectTimeout: const Duration(seconds: 5),
+    receiveTimeout: const Duration(seconds: 3),
+  ));
+
+  dio.interceptors.add(LogInterceptor(responseBody: true));
+
+  return dio;
+}
+
+// Create a custom LogInterceptor using the logger object
+class CustomLogInterceptor extends Interceptor {
+  @override
+  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
+    logger.info('REQUEST[${options.method}] => PATH: ${options.path}');
+    return super.onRequest(options, handler);
+  }
+
+  @override
+  // ignore: strict_raw_type
+  void onResponse(Response response, ResponseInterceptorHandler handler) {
+    logger.info(
+      'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}',
+    );
+    return super.onResponse(response, handler);
+  }
+
+  @override
+  void onError(DioException err, ErrorInterceptorHandler handler) {
+    logger.severe(
+      'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}',
+    );
+    return super.onError(err, handler);
+  }
+}
diff --git a/frontend/lib/providers/utility.dart b/frontend/lib/providers/utility.dart
new file mode 100644
index 0000000..ea1e8b1
--- /dev/null
+++ b/frontend/lib/providers/utility.dart
@@ -0,0 +1,10 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+part 'utility.g.dart';
+
+@riverpod
+Future<SharedPreferencesAsync> sharedPreferencesAsync(Ref ref) async {
+  return SharedPreferencesAsync();
+}
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index 83ca870..28bbd5c 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -230,6 +230,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.0.1"
+  dio:
+    dependency: "direct main"
+    description:
+      name: dio
+      sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.8.0+1"
+  dio_web_adapter:
+    dependency: transitive
+    description:
+      name: dio_web_adapter
+      sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.0"
   drift:
     dependency: transitive
     description:
@@ -333,6 +349,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.1.3"
+  go_router:
+    dependency: "direct main"
+    description:
+      name: go_router
+      sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651"
+      url: "https://pub.dev"
+    source: hosted
+    version: "14.8.0"
   graphs:
     dependency: transitive
     description:
@@ -389,6 +413,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.9.0"
+  jwt_decoder:
+    dependency: "direct main"
+    description:
+      name: jwt_decoder
+      sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.1"
   leak_tracker:
     dependency: transitive
     description:
@@ -422,7 +454,7 @@ packages:
     source: hosted
     version: "5.1.1"
   logging:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: logging
       sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index b69c28e..2e16b7c 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -8,10 +8,14 @@ environment:
   sdk: ^3.6.0
 
 dependencies:
+  dio: ^5.8.0+1
   drift_flutter: ^0.2.4
   flutter:
     sdk: flutter
   flutter_riverpod: ^2.6.1
+  go_router: ^14.8.0
+  jwt_decoder: ^2.0.1
+  logging: ^1.3.0
   riverpod_annotation: ^2.6.1
   shared_models:
     path: ../shared_models
diff --git a/shared_models/lib/fart_logger.dart b/shared_models/lib/fart_logger.dart
new file mode 100644
index 0000000..1430ad1
--- /dev/null
+++ b/shared_models/lib/fart_logger.dart
@@ -0,0 +1,54 @@
+import 'dart:io';
+
+import 'package:logging/logging.dart';
+
+class FartLogger {
+  static void listen({required bool isDevelopment}) {
+    final logLevel = Platform.environment['LOG_LEVEL'] ?? (isDevelopment ? 'FINEST' : 'INFO');
+    Logger.root.level =
+        Level.LEVELS.firstWhere((l) => l.name == logLevel, orElse: () => Level.INFO); // defaults to Level.INFO
+    Logger.root.onRecord.listen((record) {
+      _writeLogRecord(record, record.level.value >= Level.SEVERE.value ? stderr : stdout);
+    });
+  }
+
+  static void _writeLogRecord(LogRecord record, IOSink iosink) {
+    // Write the basic log message with colored level
+    iosink.writeln(
+      '[${_getColoredLevel(record.level.name)}]:[${record.loggerName}] '
+      '${record.time}: ${record.message}',
+    );
+
+    // Additional details for severe logs
+    if (record.level.value >= Level.SEVERE.value) {
+      iosink.writeln(
+        '[${_getColoredLevel(record.level.name)}]:[${record.loggerName}] '
+        '${record.error?.toString() ?? "No error provided"}\n'
+        '${record.stackTrace?.toString() ?? "No trace provided"}',
+      );
+    }
+  }
+
+  static void printLevels() {
+    for (final lvl in Level.LEVELS) {
+      _writeLogRecord(LogRecord(lvl, 'Test message', 'main'), stdout);
+    }
+  }
+
+  static const Map<String, String> _levelColors = {
+    'FINEST': '\x1B[1;37m', // White
+    'FINER': '\x1B[1;38m', // Gray
+    'FINE': '\x1B[1;35m', // Purple
+    'CONFIG': '\x1B[1;36m', // Cyan
+    'INFO': '\x1B[1;32m', // Green
+    'WARNING': '\x1B[1;33m', // Yellow
+    'SEVERE': '\x1B[1;31m', // Red
+    'SHOUT': '\x1B[1;38;5;52m\x1B[1;48;5;213m', // Red on pink
+  };
+
+  static const String _resetColor = '\x1B[0m';
+
+  static String _getColoredLevel(String levelName) {
+    return '${_levelColors[levelName] ?? ''}$levelName$_resetColor';
+  }
+}
diff --git a/shared_models/pubspec.lock b/shared_models/pubspec.lock
index af97ed3..a75c4b0 100644
--- a/shared_models/pubspec.lock
+++ b/shared_models/pubspec.lock
@@ -263,7 +263,7 @@ packages:
     source: hosted
     version: "5.1.1"
   logging:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: logging
       sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
diff --git a/shared_models/pubspec.yaml b/shared_models/pubspec.yaml
index 840151c..ab498b3 100644
--- a/shared_models/pubspec.yaml
+++ b/shared_models/pubspec.yaml
@@ -8,6 +8,7 @@ environment:
 
 dependencies:
   json_annotation: ^4.9.0
+  logging: ^1.3.0
 
 dev_dependencies:
   json_serializable: ^6.9.3