diff --git a/.gitignore b/.gitignore
index 3e2ac3f..e75444d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,6 +68,9 @@ app.*.map.json
 # Misc
 ###
 
+# Dartfrog
+**/.dart_frog/**
+
 # Direnv
 **/.direnv/**
 
diff --git a/.ignore b/.ignore
index b4f9b4a..e5c3f70 100644
--- a/.ignore
+++ b/.ignore
@@ -2,3 +2,7 @@
 LICENSE
 **/.ignore
 **/.gitignore
+
+# Dont edit these often, so its just noise in the file picker
+backend/test_e2e/pubspec.yaml
+backend/scripts/pubspec.yaml
diff --git a/README.md b/README.md
index 0a64415..98d3c78 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Dev is done all in dart, with the flutter framework as a frontend. The
 backend uses [dart_frog](), a dart backend framework with a focus on developer
 experience. This whole stack revolves around my Dart expertise, so its not about
 being the fastest stack, but the quickest stack I can build in. Because building
-a product is like a fart: its all about follow-through.
+a product is like a fart: You can't be sure its not crap until the follow-through.
 
 ### Installing
 
@@ -16,6 +16,38 @@ If you have the nix package manager and direnv setup, its as easy as running `di
 
 If not, well, you need to setup `flutter`, `dart` (should get installed with flutter), and `dart_frog`
 
+### Running
+
+This codebase makes use of [RPS](https://pub.dev/packages/rps/install), just to standardize calling
+various tools, like Make, but for dart.
+
+- Frontend is started with `cd frontend` and `flutter run` (not included in rps because there are lots of cli flags I change often with flutter run)
+- Backend is started with `cd backend` and `rps dev` or `dart_frog dev`
+- The `build_runners` (codegen) for each subproject can all be run `cd backend` and `rps watch` for development or `rps build` for a one off codegen
+  - If you dont want to bother installing rps, you can run the script directly:
+```sh
+dart backend/scripts/run_build_runner.dart
+```
+
+And if you dont want to run all three watchers, you can just run an individual one with
+```sh
+dart run build_runner watch # or other args
+```
+
+### Testing
+
+#### Backend
+
+End to end tests can be run with `cd backend` and `rps e2e`.
+
+All end to end tests are located in the `backend/test_e2e/tests` folder.
+There is a helper script that compiles and launches the backend and runs some end to end tests through.
+
+#### Frontend
+
+:|
+
+
 ## Frontend `./frontend/`
 
 WIP
diff --git a/backend/.dart_frog/server.dart b/backend/.dart_frog/server.dart
deleted file mode 100644
index 2c08f7c..0000000
--- a/backend/.dart_frog/server.dart
+++ /dev/null
@@ -1,57 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY BY HAND
-// ignore_for_file: type=lint, implicit_dynamic_list_literal
-
-import 'dart:io';
-
-import 'package:dart_frog/dart_frog.dart';
-
-import '../main.dart' as entrypoint;
-import '../routes/index.dart' as index;
-import '../routes/create_room.dart' as create_room;
-import '../routes/room/[roomCode]/join.dart' as room_$room_code_join;
-import '../routes/auth/index.dart' as auth_index;
-
-import '../routes/_middleware.dart' as middleware;
-import '../routes/room/[roomCode]/_middleware.dart' as room_$room_code_middleware;
-
-void main() async {
-  final address = InternetAddress.tryParse('') ?? InternetAddress.anyIPv6;
-  final port = int.tryParse(Platform.environment['PORT'] ?? '8080') ?? 8080;
-  hotReload(() => createServer(address, port));
-}
-
-Future<HttpServer> createServer(InternetAddress address, int port) {
-  final handler = Cascade().add(buildRootHandler()).handler;
-  return entrypoint.run(handler, address, port);
-}
-
-Handler buildRootHandler() {
-  final pipeline = const Pipeline().addMiddleware(middleware.middleware);
-  final router = Router()
-    ..mount('/auth', (context) => buildAuthHandler()(context))
-    ..mount('/room/<roomCode>', (context,roomCode,) => buildRoom$roomCodeHandler(roomCode,)(context))
-    ..mount('/', (context) => buildHandler()(context));
-  return pipeline.addHandler(router);
-}
-
-Handler buildAuthHandler() {
-  final pipeline = const Pipeline();
-  final router = Router()
-    ..all('/', (context) => auth_index.onRequest(context,));
-  return pipeline.addHandler(router);
-}
-
-Handler buildRoom$roomCodeHandler(String roomCode,) {
-  final pipeline = const Pipeline().addMiddleware(room_$room_code_middleware.middleware);
-  final router = Router()
-    ..all('/join', (context) => room_$room_code_join.onRequest(context,roomCode,));
-  return pipeline.addHandler(router);
-}
-
-Handler buildHandler() {
-  final pipeline = const Pipeline();
-  final router = Router()
-    ..all('/', (context) => index.onRequest(context,))..all('/create_room', (context) => create_room.onRequest(context,));
-  return pipeline.addHandler(router);
-}
-
diff --git a/backend/analysis_options.yaml b/backend/analysis_options.yaml
index 51f78f8..7afe797 100644
--- a/backend/analysis_options.yaml
+++ b/backend/analysis_options.yaml
@@ -6,6 +6,7 @@ linter:
     prefer_single_quotes: true
     public_member_api_docs: false
     lines_longer_than_80_chars: false
+    avoid_redundant_argument_values: false
 
 analyzer:
   exclude:
diff --git a/backend/lib/authenticator.dart b/backend/lib/authenticator.dart
index 5ea1c74..bc52c90 100644
--- a/backend/lib/authenticator.dart
+++ b/backend/lib/authenticator.dart
@@ -4,6 +4,7 @@ import 'package:backend/database.dart';
 import 'package:backend/service/db_access.dart';
 import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
 import 'package:logging/logging.dart';
+import 'package:shared_models/jwt.dart';
 import 'package:shared_models/user.dart';
 
 final jwtSecret = _getSecret();
@@ -18,21 +19,16 @@ enum JWTTokenStatus {
 }
 
 class Authenticator {
-  Future<String?> generateToken(CreateUserRequest req) async {
+  Future<(String?, User?)> generateToken(CreateUserRequest req) async {
     final newUser = await Db.createUser(username: req.username, roomCode: req.roomCode);
-    if (newUser == null) return null;
+    if (newUser == null) return (null, null);
 
     final iat = DateTime.now().millisecondsSinceEpoch ~/ 1000;
     final jwt = JWT(
-      {
-        'uid': newUser.uuid,
-        'roomUuid': newUser.gameRoomUuid,
-        'iat': iat,
-        'exp': iat + expTimeSecs,
-      },
+      JWTBody(uuid: newUser.uuid, roomUuid: newUser.gameRoomUuid, iat: iat, exp: iat + expTimeSecs).toJson(),
     );
 
-    return jwt.sign(SecretKey(jwtSecret));
+    return (jwt.sign(SecretKey(jwtSecret)), newUser);
   }
 
   Future<(User?, JWTTokenStatus)> verifyToken(
@@ -45,18 +41,15 @@ class Authenticator {
         SecretKey(jwtSecret),
       );
 
-      final payloadData = payload.payload as Map<String, dynamic>;
+      final jwt = JWTBody.fromJson(payload.payload as Map<String, dynamic>);
 
-      final iat = payloadData['iat'] as int;
-      final exp = payloadData['exp'] as int;
-
-      if (iat + expTimeSecs != exp || DateTime.now().millisecondsSinceEpoch ~/ 1000 > exp) {
+      if (jwt.iat + expTimeSecs != jwt.exp || DateTime.now().millisecondsSinceEpoch ~/ 1000 > jwt.exp) {
         return (null, JWTTokenStatus.expired);
       }
 
-      final uuid = payloadData['uuid'] as String;
-      return (await Db.getUser(uuid), JWTTokenStatus.valid);
+      return (await Db.getUserById(jwt.uuid), JWTTokenStatus.valid);
     } catch (e) {
+      log.fine('Error verifying token', e);
       return (null, JWTTokenStatus.invalid);
     }
   }
diff --git a/backend/lib/middleware/auth_middleware.dart b/backend/lib/middleware/auth_middleware.dart
index ec0fc44..294f7a0 100644
--- a/backend/lib/middleware/auth_middleware.dart
+++ b/backend/lib/middleware/auth_middleware.dart
@@ -22,7 +22,7 @@ Middleware tokenAuthMiddleware({
         }
         final auth = context.read<Authenticator>();
         // use `auth.verifyToken(token)` to check the jwt that came in the request header bearer
-        final authHeader = context.request.headers['authorization'];
+        final authHeader = context.request.headers['authorization'] ?? context.request.headers['Authorization'];
         final auths = authHeader?.split(' ');
         if (authHeader == null || !authHeader.startsWith('Bearer ') || auths == null || auths.length != 2) {
           log.fine('Denied request - No Auth - ${context.request.method.value} ${context.request.uri.path}');
@@ -34,7 +34,8 @@ Middleware tokenAuthMiddleware({
 
         if (user == null) {
           log.fine(
-              'Denied request - Bad Auth:$tokStatus - ${context.request.method.value} ${context.request.uri.path}, no auth');
+            'Denied request - Bad Auth:$tokStatus - ${context.request.method.value} ${context.request.uri.path}',
+          );
           return Response(statusCode: HttpStatus.unauthorized);
         }
 
diff --git a/backend/lib/service/db_access.dart b/backend/lib/service/db_access.dart
index b6966d5..b576ff0 100644
--- a/backend/lib/service/db_access.dart
+++ b/backend/lib/service/db_access.dart
@@ -8,9 +8,9 @@ final log = Logger('Db');
 class Db {
   static final _db = AppDatabase();
 
-  static Future<User> getUser(String uuid) {
+  static Future<User?> getUserById(String uuid) {
     log.finer('Getting user $uuid');
-    return _db.managers.users.filter((f) => f.uuid.equals(uuid)).getSingle();
+    return _db.managers.users.filter((f) => f.uuid.equals(uuid)).getSingleOrNull();
   }
 
   static Future<User?> createUser({required String username, required String roomCode}) async {
@@ -32,14 +32,21 @@ class Db {
     });
   }
 
-  static Future<GameRoom> createRoom({required String roomCode}) => _db.managers.gameRooms
-          .createReturning(
+  static Future<GameRoom?> createRoom({required String roomCode}) => _db.managers.gameRooms
+          .createReturningOrNull(
         (o) => o(createdAt: Value(DateTime.now()), status: GameStatus.open, uuid: const Uuid().v4(), code: roomCode),
       )
           .catchError(
         (Object err) {
           log.severe('Failed to create room', err, StackTrace.current);
-          throw Exception(err.toString());
+          return null;
         },
       );
+
+  static Future<GameRoom?> getRoomByCode(String? roomCode) async {
+    final room = await _db.managers.gameRooms
+        .filter((f) => f.code.equals(roomCode) & f.status.isIn([GameStatus.open, GameStatus.running]))
+        .getSingleOrNull();
+    return room;
+  }
 }
diff --git a/backend/pubspec.lock b/backend/pubspec.lock
index 86881c6..a89e298 100644
--- a/backend/pubspec.lock
+++ b/backend/pubspec.lock
@@ -302,6 +302,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.3.0"
+  http:
+    dependency: "direct dev"
+    description:
+      name: http
+      sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
   http_methods:
     dependency: transitive
     description:
diff --git a/backend/pubspec.yaml b/backend/pubspec.yaml
index bb327b2..eddc4d0 100644
--- a/backend/pubspec.yaml
+++ b/backend/pubspec.yaml
@@ -20,6 +20,16 @@ dependencies:
 dev_dependencies:
   build_runner: ^2.4.14
   drift_dev: ^2.24.0
+  # For e2e testing
+  http: ^1.3.0
   mocktail: ^1.0.3
   test: ^1.25.5
   very_good_analysis: ^5.1.0
+
+scripts:
+  run_build: dart scripts/run_build_runner.dart
+  run_watch: dart scripts/run_build_runner.dart --watch
+  build: dart_frog build
+  dev: dart_frog dev
+  e2e: dart scripts/test_runner.dart
+  test: dart test
diff --git a/backend/routes/create_room.dart b/backend/routes/create_room.dart
index 6ff734c..9ae7641 100644
--- a/backend/routes/create_room.dart
+++ b/backend/routes/create_room.dart
@@ -6,7 +6,7 @@ import 'package:dart_frog/dart_frog.dart';
 import 'package:logging/logging.dart';
 import 'package:shared_models/room.dart';
 
-final log = Logger('create_room');
+final log = Logger('create_room/');
 
 Future<Response> onRequest(RequestContext context) async {
   // Only allow POST requests
@@ -28,6 +28,17 @@ Future<Response> onRequest(RequestContext context) async {
     // Create the room
     final room = await Db.createRoom(roomCode: roomCode);
 
+    if (room == null) {
+      return Response.json(
+        statusCode: HttpStatus.internalServerError,
+        body: CreateRoomResponse(
+          error: 'Unexpected error: unable to create room',
+          success: false,
+          roomCode: null,
+        ).toJson(),
+      );
+    }
+
     // Return the room code
     return Response.json(
       body: CreateRoomResponse(success: true, roomCode: room.code).toJson(),
diff --git a/backend/routes/index.dart b/backend/routes/health.dart
similarity index 64%
rename from backend/routes/index.dart
rename to backend/routes/health.dart
index a538147..fb0ac8c 100644
--- a/backend/routes/index.dart
+++ b/backend/routes/health.dart
@@ -1,5 +1,5 @@
 import 'package:dart_frog/dart_frog.dart';
 
 Response onRequest(RequestContext context) {
-  return Response(body: 'Welcome to Dart Frog!');
+  return Response(statusCode: 200);
 }
diff --git a/backend/routes/auth/index.dart b/backend/routes/join_room.dart
similarity index 64%
rename from backend/routes/auth/index.dart
rename to backend/routes/join_room.dart
index d9cf4e6..d55ddf1 100644
--- a/backend/routes/auth/index.dart
+++ b/backend/routes/join_room.dart
@@ -5,7 +5,7 @@ import 'package:dart_frog/dart_frog.dart';
 import 'package:logging/logging.dart';
 import 'package:shared_models/user.dart';
 
-final log = Logger('auth/');
+final log = Logger('join_room/');
 
 Future<Response> onRequest(RequestContext context) async {
   // Only allow POST requests
@@ -20,33 +20,28 @@ Future<Response> onRequest(RequestContext context) async {
 
     // Generate token
     final authenticator = context.read<Authenticator>();
-    final token = await authenticator.generateToken(createUserReq);
+    final (token, user) = await authenticator.generateToken(createUserReq);
 
-    if (token == null) {
+    if (token == null || user == null) {
       final body = CreateUserResponse(
         success: false,
         token: null,
-        error: 'Room ${createUserReq.roomCode} requested is not available',
+        error: user == null ? 'Room ${createUserReq.roomCode} requested is not available' : 'Unexpected error occurred',
+        uuid: null,
       ).toJson();
       return Response.json(
-        statusCode: HttpStatus.badRequest,
+        statusCode: user == null ? HttpStatus.badRequest : HttpStatus.internalServerError,
         body: body,
       );
     }
 
     // Return the token
     return Response.json(
-      body: CreateUserResponse(token: token, success: true).toJson(),
+      body: CreateUserResponse(token: token, success: true, uuid: user.uuid).toJson(),
     );
-    // }
-    //  on JWTParseException {
-    //   return Response.json(
-    //     statusCode: HttpStatus.badRequest,
-    //     body: {'error': 'Username is required'},
-    //   );
   } catch (e) {
-    log.severe('Error:', e);
-    final body = CreateUserResponse(success: false, token: null, error: 'Internal server error').toJson();
+    log.severe('Error:', e, StackTrace.current);
+    final body = CreateUserResponse(success: false, token: null, error: 'Internal server error', uuid: null).toJson();
     return Response.json(
       statusCode: HttpStatus.internalServerError,
       body: body,
diff --git a/backend/routes/room/[roomCode]/ping.dart b/backend/routes/room/[roomCode]/ping.dart
new file mode 100644
index 0000000..544f9cd
--- /dev/null
+++ b/backend/routes/room/[roomCode]/ping.dart
@@ -0,0 +1,10 @@
+import 'package:backend/service/db_access.dart';
+import 'package:dart_frog/dart_frog.dart';
+
+Future<Response> onRequest(RequestContext context, String roomCode) async {
+  final gameRoom = await Db.getRoomByCode(roomCode);
+  if (gameRoom == null) {
+    return Response(statusCode: 404);
+  }
+  return Response(statusCode: 200);
+}
diff --git a/backend/scripts/pubspec.lock b/backend/scripts/pubspec.lock
new file mode 100644
index 0000000..2816fe3
--- /dev/null
+++ b/backend/scripts/pubspec.lock
@@ -0,0 +1,402 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "79.0.0"
+  _macros:
+    dependency: transitive
+    description: dart
+    source: sdk
+    version: "0.3.3"
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.2.0"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.6.0"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.12.0"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.19.1"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.2"
+  coverage:
+    dependency: transitive
+    description:
+      name: coverage
+      sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.11.1"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.6"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.1"
+  frontend_server_client:
+    dependency: transitive
+    description:
+      name: frontend_server_client
+      sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.0"
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.3"
+  http:
+    dependency: "direct dev"
+    description:
+      name: http
+      sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  http_multi_server:
+    dependency: transitive
+    description:
+      name: http_multi_server
+      sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.2.2"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.2"
+  io:
+    dependency: transitive
+    description:
+      name: io
+      sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.5"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.1"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  macros:
+    dependency: transitive
+    description:
+      name: macros
+      sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.3-main.0"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.12.17"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.16.0"
+  mime:
+    dependency: transitive
+    description:
+      name: mime
+      sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.0"
+  node_preamble:
+    dependency: transitive
+    description:
+      name: node_preamble
+      sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.2"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.9.1"
+  pool:
+    dependency: transitive
+    description:
+      name: pool
+      sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.5.1"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.5"
+  shelf:
+    dependency: transitive
+    description:
+      name: shelf
+      sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.2"
+  shelf_packages_handler:
+    dependency: transitive
+    description:
+      name: shelf_packages_handler
+      sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.2"
+  shelf_static:
+    dependency: transitive
+    description:
+      name: shelf_static
+      sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.3"
+  shelf_web_socket:
+    dependency: transitive
+    description:
+      name: shelf_web_socket
+      sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.1"
+  source_map_stack_trace:
+    dependency: transitive
+    description:
+      name: source_map_stack_trace
+      sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  source_maps:
+    dependency: transitive
+    description:
+      name: source_maps
+      sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.10.13"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.1"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.12.1"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.1"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.2"
+  test:
+    dependency: "direct dev"
+    description:
+      name: test
+      sha256: "8391fbe68d520daf2314121764d38e37f934c02fd7301ad18307bd93bd6b725d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.25.14"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.4"
+  test_core:
+    dependency: transitive
+    description:
+      name: test_core
+      sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.8"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.0"
+  vm_service:
+    dependency: transitive
+    description:
+      name: vm_service
+      sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
+      url: "https://pub.dev"
+    source: hosted
+    version: "15.0.0"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
+  web:
+    dependency: transitive
+    description:
+      name: web
+      sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  web_socket:
+    dependency: transitive
+    description:
+      name: web_socket
+      sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.6"
+  web_socket_channel:
+    dependency: transitive
+    description:
+      name: web_socket_channel
+      sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.2"
+  webkit_inspection_protocol:
+    dependency: transitive
+    description:
+      name: webkit_inspection_protocol
+      sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.3"
+sdks:
+  dart: ">=3.5.0 <4.0.0"
diff --git a/backend/scripts/pubspec.yaml b/backend/scripts/pubspec.yaml
new file mode 100644
index 0000000..9fb8a2d
--- /dev/null
+++ b/backend/scripts/pubspec.yaml
@@ -0,0 +1,8 @@
+name: "e2e_test_dart_runner"
+environment:
+  sdk: ">=3.0.0 <4.0.0"
+
+dev_dependencies:
+  test: ^1.24.0
+  http: ^1.1.0
+
diff --git a/backend/scripts/run_build_runner.dart b/backend/scripts/run_build_runner.dart
new file mode 100644
index 0000000..432c82c
--- /dev/null
+++ b/backend/scripts/run_build_runner.dart
@@ -0,0 +1,178 @@
+// ignore_for_file: avoid_print
+
+import 'dart:async';
+import 'dart:io';
+
+const packages = ['backend', 'frontend', 'shared_models'];
+Map<int, Process> runnerWatchProcesses = {};
+
+void main(List<String> args) async {
+  // Get args and create bool for if `--watch` passed in
+  final watch = args.contains('--watch');
+
+  if (watch) {
+    print('šŸ‘€ Running build_runner in watch mode for all packages...\n');
+  } else {
+    print('šŸƒ Running build_runner in build mode for all packages...\n');
+  }
+
+  final projectRootPath = await Process.run(
+    'git',
+    ['rev-parse', '--show-toplevel'],
+  ).then((result) => result.stdout.toString().trim());
+
+  for (final package in packages) {
+    unawaited(runBuildRunner('$projectRootPath/$package', watch: watch));
+  }
+  await Future<void>.delayed(const Duration(seconds: 1));
+
+  var i = 0;
+  var j = 0;
+  while (runnerWatchProcesses.isNotEmpty) {
+    await Future<void>.delayed(const Duration(seconds: 1));
+    i++;
+
+    if (i == 10) {
+      if (!watch) print('   Waiting for build to finish... ${runnerWatchProcesses.keys}');
+      if (packages.length != runnerWatchProcesses.entries.length) {
+        j++;
+      } else {
+        j = 0;
+      }
+      if (j == 2) {
+        print(
+          'Missing build runners for too long in tracked processes. Something probably went wrong. Check logs and fix the script.',
+        );
+        exitRunners();
+      }
+    }
+  }
+
+  print('\nāœ… All build_runner tasks completed successfully!');
+}
+
+Future<void> runBuildRunner(String packagePath, {required bool watch}) async {
+  final package = packagePath.split('/').last;
+  print('šŸ“¦ Starting build_runner for $package...');
+  Future<(Process, bool)> buildRunnerFunc() => watch ? _runBuildRunnerWatch(packagePath) : _runBuildRunner(packagePath);
+  const maxAttempts = 5;
+
+  Future<void> attemptRecover(Object? e, int i) async {
+    try {
+      print('\nāš ļø  Error in $package:\n$e\nAttempting recovery (${i + 1} / $maxAttempts)...\n');
+
+      print('   Waiting 10 seconds before retry...');
+      await Future<void>.delayed(const Duration(seconds: 1));
+
+      // Recovery attempt
+      print('   Running pub get...');
+      await Process.run(
+        package == 'frontend' ? 'flutter' : 'dart',
+        ['pub', 'get'],
+        workingDirectory: packagePath,
+      );
+
+      print('   Retrying build_runner...');
+      return;
+    } catch (e) {
+      print('Error while trying to recover $package: $e');
+    }
+  }
+
+  Object? err;
+  for (var i = 0; i < maxAttempts; i++) {
+    try {
+      final (process, needsRestart) = await buildRunnerFunc();
+      final removed = runnerWatchProcesses.remove(process.pid);
+      assert(removed != null, true);
+      if (needsRestart) {
+        await attemptRecover(process.stderr, i);
+      } else {
+        return;
+      }
+    } catch (e) {
+      await attemptRecover(e, i);
+    }
+  }
+  print('\nāŒ Failed to recover $packagePath: $err');
+  exitRunners();
+}
+
+Future<(Process, bool)> _runBuildRunner(String package) async {
+  final process = await Process.start(
+    'dart',
+    ['run', 'build_runner', 'build', '--delete-conflicting-outputs'],
+    workingDirectory: package,
+  );
+
+  runnerWatchProcesses.addAll({process.pid: process});
+
+  final outputLines = <String>[];
+  final errorLines = <String>[];
+
+  process.stdout.transform(const SystemEncoding().decoder).listen(outputLines.add);
+
+  process.stderr.transform(const SystemEncoding().decoder).listen(errorLines.add);
+
+  final exitCode = await process.exitCode;
+
+  if (exitCode != 0) {
+    // Print filtered error output
+    final relevantErrors = errorLines
+        .where((line) => line.contains('ERROR') || line.contains('Exception') || line.contains('Failed'))
+        .join('\n');
+
+    print('Build runner failed for $package:\n$relevantErrors');
+    return (process, true);
+  }
+
+  // Print success with minimal output
+  final successMessage =
+      outputLines.where((line) => line.contains('Succeeded') || line.contains('Generated')).join('\n').trim();
+
+  print('   ${successMessage.isEmpty ? "Completed successfully" : successMessage}');
+  return (process, false);
+}
+
+Future<(Process, bool)> _runBuildRunnerWatch(String packagePath) async {
+  final package = packagePath.split('/').last;
+  final process = await Process.start(
+    'dart',
+    ['run', 'build_runner', 'watch', '--delete-conflicting-outputs'],
+    workingDirectory: packagePath,
+  );
+
+  runnerWatchProcesses.addAll({process.pid: process});
+
+  var restart = false;
+  process.stdout.transform(const SystemEncoding().decoder).listen((line) {
+    if (line.contains('Succeeded') || line.contains('Generated')) {
+      print('šŸŽÆ [${package.toUpperCase()}] - ${line.trim()}');
+    }
+    if (line.contains('SEVERE')) {
+      print('šŸš© [${package.toUpperCase()}] - ${line.trim()}');
+      restart = true;
+    }
+  });
+
+  process.stderr.transform(const SystemEncoding().decoder).listen((line) {
+    if (line.contains('ERROR') || line.contains('Exception') || line.contains('Failed')) {
+      print('šŸŽÆ [${package.toUpperCase()}] - ${line.trim()}');
+      restart = true;
+    }
+  });
+
+  final code = await process.exitCode;
+  if (!restart) {
+    restart = code != 0;
+  }
+  return (process, restart);
+}
+
+void exitRunners() {
+  print('āŒ Killing processes');
+  for (final pEntry in runnerWatchProcesses.entries) {
+    pEntry.value.kill();
+  }
+  exit(1);
+}
diff --git a/backend/scripts/test_runner.dart b/backend/scripts/test_runner.dart
new file mode 100644
index 0000000..ccfe4e9
--- /dev/null
+++ b/backend/scripts/test_runner.dart
@@ -0,0 +1,76 @@
+import 'dart:async';
+import 'dart:io';
+
+void main() async {
+  final projectRootPath = await Process.run(
+    'git',
+    ['rev-parse', '--show-toplevel'],
+  ).then((result) => result.stdout.toString().trim());
+
+  final backendPath = '$projectRootPath/backend';
+  // Start the server
+  print('Starting Dart Frog server...');
+  final build = await Process.start(
+    'dart_frog',
+    ['build'],
+    workingDirectory: backendPath,
+    mode: ProcessStartMode.inheritStdio,
+  );
+
+  List<String> serverLogs = [];
+  final server = await Process.start('dart', ['build/bin/server.dart'], workingDirectory: backendPath);
+  server.stdout.transform(const SystemEncoding().decoder).listen((line) => serverLogs.add(line));
+  server.stderr.transform(const SystemEncoding().decoder).listen((line) => serverLogs.add(line));
+
+  // Wait for server to be ready
+  print('Waiting for server to be ready...');
+  await _waitForServer();
+
+  print('Running tests...');
+  try {
+    // Run the tests
+    final testResult = await Process.run('dart', ['test', '$backendPath/test_e2e/tests/']);
+    stdout.write(testResult.stdout);
+    stderr.write(testResult.stderr);
+
+    // Kill the server regardless of test result
+    server.kill();
+
+    if (testResult.exitCode != 0) {
+      final sub = serverLogs.length > 10 ? serverLogs.length - 10 : 0;
+      stdout.write("Server logs:\n${serverLogs.sublist(sub).join('\n')}");
+    }
+
+    // Exit with the same code as the test process
+    exit(testResult.exitCode);
+  } catch (e) {
+    print('Error running tests: $e');
+    build.kill();
+    server.kill();
+
+    exit(1);
+  }
+}
+
+Future<void> _waitForServer() async {
+  final client = HttpClient();
+  const maxAttempts = 30; // 30 seconds timeout
+  var attempts = 0;
+
+  while (attempts < maxAttempts) {
+    try {
+      final request =
+          await client.getUrl(Uri.parse('http://localhost:8080/health')).timeout(const Duration(seconds: 1));
+      final response = await request.close();
+      await response.drain<void>();
+      if (response.statusCode == 200) {
+        print('Server is ready!');
+        return;
+      }
+    } catch (e) {
+      attempts++;
+      await Future.delayed(const Duration(seconds: 1));
+    }
+  }
+  throw Exception('Server failed to start after $maxAttempts seconds');
+}
diff --git a/backend/test/routes/index_test.dart b/backend/test/routes/health_test.dart
similarity index 66%
rename from backend/test/routes/index_test.dart
rename to backend/test/routes/health_test.dart
index 6936e38..44ccd82 100644
--- a/backend/test/routes/index_test.dart
+++ b/backend/test/routes/health_test.dart
@@ -4,19 +4,20 @@ import 'package:dart_frog/dart_frog.dart';
 import 'package:mocktail/mocktail.dart';
 import 'package:test/test.dart';
 
-import '../../routes/index.dart' as route;
+import '../../routes/health.dart' as route;
 
 class _MockRequestContext extends Mock implements RequestContext {}
 
 void main() {
-  group('GET /', () {
-    test('responds with a 200 and "Welcome to Dart Frog!".', () {
+  group('GET /health', () {
+    test('responds with a 200 and an empty body.', () async {
       final context = _MockRequestContext();
       final response = route.onRequest(context);
+      final body = await response.body();
       expect(response.statusCode, equals(HttpStatus.ok));
       expect(
-        response.body(),
-        completion(equals('Welcome to Dart Frog!')),
+        body,
+        '',
       );
     });
   });
diff --git a/backend/test_e2e/pubspec.lock b/backend/test_e2e/pubspec.lock
new file mode 100644
index 0000000..7528ee6
--- /dev/null
+++ b/backend/test_e2e/pubspec.lock
@@ -0,0 +1,560 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "79.0.0"
+  _macros:
+    dependency: transitive
+    description: dart
+    source: sdk
+    version: "0.3.3"
+  adaptive_number:
+    dependency: transitive
+    description:
+      name: adaptive_number
+      sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.0"
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.2.0"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.6.0"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.12.0"
+  backend:
+    dependency: "direct main"
+    description:
+      path: ".."
+      relative: true
+    source: path
+    version: "0.0.1+1"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.2"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.19.1"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.2"
+  coverage:
+    dependency: transitive
+    description:
+      name: coverage
+      sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.11.1"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.6"
+  dart_frog:
+    dependency: transitive
+    description:
+      name: dart_frog
+      sha256: "569db68a710bcadf96d8addc8988d09a93c4a9521cb6467c2df5ee0ab939c8a4"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  dart_frog_auth:
+    dependency: transitive
+    description:
+      name: dart_frog_auth
+      sha256: a4c05e6764a82f8997dee9fb68661a473afea92527a2dd8a6677e5a1b454b463
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.0"
+  dart_jsonwebtoken:
+    dependency: transitive
+    description:
+      name: dart_jsonwebtoken
+      sha256: "00a0812d2aeaeb0d30bcbc4dd3cee57971dbc0ab2216adf4f0247f37793f15ef"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.17.0"
+  drift:
+    dependency: transitive
+    description:
+      name: drift
+      sha256: "76f23535e19a9f2be92f954e74d8802e96f526e5195d7408c1a20f6659043941"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.24.0"
+  ed25519_edwards:
+    dependency: transitive
+    description:
+      name: ed25519_edwards
+      sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.1"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.3"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.1"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
+  frontend_server_client:
+    dependency: transitive
+    description:
+      name: frontend_server_client
+      sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.0"
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.3"
+  hotreloader:
+    dependency: transitive
+    description:
+      name: hotreloader
+      sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.3.0"
+  http:
+    dependency: "direct main"
+    description:
+      name: http
+      sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  http_methods:
+    dependency: transitive
+    description:
+      name: http_methods
+      sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
+  http_multi_server:
+    dependency: transitive
+    description:
+      name: http_multi_server
+      sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.2.2"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.2"
+  io:
+    dependency: transitive
+    description:
+      name: io
+      sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.5"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.1"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.9.0"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  macros:
+    dependency: transitive
+    description:
+      name: macros
+      sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.3-main.0"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.12.17"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.16.0"
+  mime:
+    dependency: transitive
+    description:
+      name: mime
+      sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.0"
+  node_preamble:
+    dependency: transitive
+    description:
+      name: node_preamble
+      sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.2"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.9.1"
+  pointycastle:
+    dependency: transitive
+    description:
+      name: pointycastle
+      sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.9.1"
+  pool:
+    dependency: transitive
+    description:
+      name: pool
+      sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.5.1"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.5"
+  shared_models:
+    dependency: transitive
+    description:
+      path: "../../shared_models"
+      relative: true
+    source: path
+    version: "1.0.0"
+  shelf:
+    dependency: transitive
+    description:
+      name: shelf
+      sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.2"
+  shelf_hotreload:
+    dependency: transitive
+    description:
+      name: shelf_hotreload
+      sha256: d7099618b18d3c63ba5272491c1812c306629495129ef9996115f0417902f963
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.5.0"
+  shelf_packages_handler:
+    dependency: transitive
+    description:
+      name: shelf_packages_handler
+      sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.2"
+  shelf_static:
+    dependency: transitive
+    description:
+      name: shelf_static
+      sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.3"
+  shelf_web_socket:
+    dependency: transitive
+    description:
+      name: shelf_web_socket
+      sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.1"
+  source_map_stack_trace:
+    dependency: transitive
+    description:
+      name: source_map_stack_trace
+      sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  source_maps:
+    dependency: transitive
+    description:
+      name: source_maps
+      sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.10.13"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.1"
+  sprintf:
+    dependency: transitive
+    description:
+      name: sprintf
+      sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.0"
+  sqlite3:
+    dependency: transitive
+    description:
+      name: sqlite3
+      sha256: "35d3726fe18ab1463403a5cc8d97dbc81f2a0b08082e8173851363fcc97b6627"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.7.2"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.12.1"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.1"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.2"
+  test:
+    dependency: "direct main"
+    description:
+      name: test
+      sha256: "8391fbe68d520daf2314121764d38e37f934c02fd7301ad18307bd93bd6b725d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.25.14"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.4"
+  test_core:
+    dependency: transitive
+    description:
+      name: test_core
+      sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.6.8"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.0"
+  uuid:
+    dependency: transitive
+    description:
+      name: uuid
+      sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.5.1"
+  vm_service:
+    dependency: transitive
+    description:
+      name: vm_service
+      sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
+      url: "https://pub.dev"
+    source: hosted
+    version: "15.0.0"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
+  web:
+    dependency: transitive
+    description:
+      name: web
+      sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  web_socket:
+    dependency: transitive
+    description:
+      name: web_socket
+      sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.1.6"
+  web_socket_channel:
+    dependency: transitive
+    description:
+      name: web_socket_channel
+      sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.2"
+  webkit_inspection_protocol:
+    dependency: transitive
+    description:
+      name: webkit_inspection_protocol
+      sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.3"
+sdks:
+  dart: ">=3.6.0 <4.0.0"
diff --git a/backend/test_e2e/pubspec.yaml b/backend/test_e2e/pubspec.yaml
new file mode 100644
index 0000000..cb25b75
--- /dev/null
+++ b/backend/test_e2e/pubspec.yaml
@@ -0,0 +1,13 @@
+name: backend_e2e_tests
+publish_to: none
+
+environment:
+  sdk: ">=3.0.0 <4.0.0"
+
+dependencies:
+  backend:
+    path: ..
+  shared_models:
+    path: ../../shared_models
+  test: ^1.24.0
+  http: ^1.1.0
diff --git a/backend/test_e2e/tests/game_room_test.dart b/backend/test_e2e/tests/game_room_test.dart
new file mode 100644
index 0000000..e0ed32e
--- /dev/null
+++ b/backend/test_e2e/tests/game_room_test.dart
@@ -0,0 +1,64 @@
+import 'dart:convert';
+
+import 'package:backend/service/db_access.dart';
+import 'package:http/http.dart' as http;
+import 'package:shared_models/room.dart';
+import 'package:shared_models/user.dart';
+import 'package:test/test.dart';
+
+void main() {
+  const baseUrl = 'http://localhost:8080'; // Adjust as needed
+
+  test('Complete room creation and ping flow', () async {
+    // 1. Create Room
+    final createRoomResponse = await http.post(
+      Uri.parse('$baseUrl/create_room'),
+    );
+
+    expect(createRoomResponse.statusCode, 200);
+
+    final createRoomData = CreateRoomResponse.fromJson(
+      json.decode(createRoomResponse.body) as Map<String, dynamic>,
+    );
+    expect(createRoomData.success, true);
+    expect(createRoomData.roomCode, isNotNull);
+
+    final roomCode = createRoomData.roomCode!;
+
+    // 2. Join Room
+    final joinRoomResponse = await http.post(
+      Uri.parse('$baseUrl/join_room'),
+      body: jsonEncode(JoinRoomRequest(username: 'testUser123', roomCode: createRoomData.roomCode!).toJson()),
+      headers: {'Content-Type': 'application/json'},
+    );
+
+    expect(joinRoomResponse.statusCode, 200);
+
+    final joinRoomData = JoinRoomResponse.fromJson(
+      json.decode(joinRoomResponse.body) as Map<String, dynamic>,
+    );
+    expect(joinRoomData.success, true);
+    expect(joinRoomData.token, isNotNull);
+    expect(joinRoomData.uuid, isNotNull);
+
+    final token = joinRoomData.token!;
+
+    // 3. Ping Room
+    final pingResponse = await http.get(
+      Uri.parse('$baseUrl/room/$roomCode/ping'),
+      headers: {
+        'Authorization': 'Bearer $token',
+      },
+    );
+
+    expect(pingResponse.statusCode, 200);
+
+    final testUser = await Db.getUserById(joinRoomData.uuid!);
+    expect(testUser, isNotNull);
+    expect(testUser!.uuid, joinRoomData.uuid);
+
+    final gameRoom = await Db.getRoomByCode(createRoomData.roomCode);
+    expect(gameRoom, isNotNull);
+    expect(gameRoom!.code, createRoomData.roomCode);
+  });
+}
diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock
index 18d0ab9..ab1692c 100644
--- a/frontend/pubspec.lock
+++ b/frontend/pubspec.lock
@@ -320,6 +320,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "4.3.0"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
   http_multi_server:
     dependency: transitive
     description:
diff --git a/shared_models/lib/jwt.dart b/shared_models/lib/jwt.dart
new file mode 100644
index 0000000..1f9291d
--- /dev/null
+++ b/shared_models/lib/jwt.dart
@@ -0,0 +1,17 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'jwt.g.dart';
+
+@JsonSerializable()
+class JWTBody {
+  String uuid;
+  String roomUuid;
+  int iat;
+  int exp;
+
+  JWTBody({required this.uuid, required this.roomUuid, required this.iat, required this.exp});
+
+  factory JWTBody.fromJson(Map<String, dynamic> json) => _$JWTBodyFromJson(json);
+
+  Map<String, dynamic> toJson() => _$JWTBodyToJson(this);
+}
diff --git a/shared_models/lib/user.dart b/shared_models/lib/user.dart
index a5d7697..a43ea1d 100644
--- a/shared_models/lib/user.dart
+++ b/shared_models/lib/user.dart
@@ -2,6 +2,8 @@ import 'package:json_annotation/json_annotation.dart';
 
 part 'user.g.dart';
 
+typedef JoinRoomRequest = CreateUserRequest;
+
 @JsonSerializable()
 class CreateUserRequest {
   final String username;
@@ -13,13 +15,16 @@ class CreateUserRequest {
   Map<String, dynamic> toJson() => _$CreateUserRequestToJson(this);
 }
 
+typedef JoinRoomResponse = CreateUserResponse;
+
 @JsonSerializable()
 class CreateUserResponse {
   final String? token;
   final String? error;
+  final String? uuid;
   final bool success;
 
-  CreateUserResponse({required this.token, required this.success, this.error});
+  CreateUserResponse({required this.token, required this.uuid, required this.success, this.error});
 
   factory CreateUserResponse.fromJson(Map<String, dynamic> json) => _$CreateUserResponseFromJson(json);