---
title: "Deploy a Dart App on Railway, Part 2"
description: "Take your Dart app to the next level"
date: 2024-12-13T00:00:00.000Z
updated: 2026-10-29T11:13:13.227Z
topics: ["Guides"]
tags: []
url: https://railway.com/guides/deploy-a-dart-app-part-2
---

# Deploy a Dart App on Railway, Part 2

On our [previous article](https://railway.com/guides/deploy-a-dart-app-part-1), we have successfully deployed a hello world Dart app to Railway using Serverpod

In this article, we’re going to actually build our small project: Trivyal, so let’s jump right in …

<aside>

ℹ️

FYI - To avoid visually overwhelming, I have not added all code blocks directly in the page. Instead, I will provide links to the relevant code to copy in the public repository.

</aside>

## Setting up Google sign in 💼

Before anything,

- Create a new [Google Cloud Project](https://console.cloud.google.com/projectcreate)
- Setup the [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent)

<aside>

ℹ️

To keep things simple we will assume we only target web for this article, but you can read the full instructions on [this article from Serverpod docs](https://medium.com/serverpod/integrating-google-sign-in-with-serverpod-authentication-part-2-6fade3099baf)

Please note that I changed the redirectUri from `/googlesignIn` in Serverpod’s article to `/googleSignIn` in this article (to save you hours from debugging a spelling error bug)

</aside>

### OAuth consent screen

After finishing OAuth consent screen setup do the following:

- Go to [Credentials](https://console.cloud.google.com/apis/credentials) and click “Create credential” ⇒ “OAuth Client ID”
- Under “Application type” choose “Web application”
- Give the client a name and leave Authorized JavaScript origins and authorized redirect URIs blank for now
- Click “Create” then “Download JSON” and save it to `trivyal_server/config/google_client_secret.json`
- Make sure to add `config/google_client_secret.json` to your `.gitignore` so you don’t accidentally add it to git

### Server code setup

We will be using `serverpod_auth_server` module to setup authentication in our server

- Add `serverpod_auth_server` to server dependencies and `serverpod_auth_client` to client dependencies
- Make sure that all related serverpod dependencies have the exact same version across the project to avoid conflicts
- Edit `trivyal_server/lib/server.dart` to match the following:

```dart
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth;  // Added!
import 'package:trivyal_server/src/web/routes/root.dart';

import 'src/generated/endpoints.dart';
import 'src/generated/protocol.dart';

// This is the starting point of your Serverpod server. In most cases, you will
// only need to make additions to this file if you add future calls,  are
// configuring Relic (Serverpod's web-server), or need custom setup work.

void run(List<String> args) async {
  // Initialize Serverpod and connect it with your generated code.
  final pod = Serverpod(
    args,
    Protocol(),
    Endpoints(),
    authenticationHandler: auth.authenticationHandler, // Added!
  );

  pod.webServer.addRoute(auth.RouteGoogleSignIn(), '/googleSignin'); // Added!

  // If you are using any future calls, they need to be registered here.
  // pod.registerFutureCall(ExampleFutureCall(), 'exampleFutureCall');

  // If you are using any future calls, they need to be registered here.
  // pod.registerFutureCall(ExampleFutureCall(), 'exampleFutureCall');

  // Serve all files in the trivyal_server/web directory.
  // This directory will be populated by Docker with our flutter web build.
  pod.webServer.addRoute(
    RouteStaticDirectory(
      serverDirectory: '.',
      serveAsRootPath: '/index.html',
      basePath: '/',
    ),
    '/*',
  );

  // Start the server.
  await pod.start();
}
```

- Run `serverpod create-migration` to create a new database migration to be applied later

## Flutter setup 📱

Let’s work on our Flutter app!

### Oauth configuration

On the [credentials](https://console.cloud.google.com/apis/credentials) page in GCP, edit the server OAuth client we created earlier

- Add the following as as Authorized JavaScript origins
    - `http://localhost:8082`
    - `http://localhost:49660`
    - `https://<railway-provided-domain>`
- Add the following as authorized redirect URIs
    - `http://localhost:8082`
    - `http://localhost:49660`
    - `http://localhost:8082/googleSignIn`
    - `https://<railway-provided-domain>/googleSignIn`

![Adding origins and redirect URIs in GCP for OAuth](https://cms.railway.com/media/87f48ce9ec852a4127c4445fd140c98951a8c2029290f83ebc5f7a719a22fe23.png)

- Save the updated JSON to `trivyal_server/config/google_client_secret.json`

### Flutter app UI

We will use `flutter_riverpod` package for dependency injection in this tutorial, but you can use any other package

- Add `flutter_riverpod` to `trivyal_flutter` dependencies
- Add `serverpod_auth_google_flutter` to `trivyal_flutter` dependencies.  We will use `SignInWithGoogleButton` from  this package to handle the sign in logic
- Add `trivyal_flutter/lib/utils/providers.dart` file with the following code:

```dart
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart';
import 'package:serverpod_flutter/serverpod_flutter.dart';
import 'package:trivyal_client/trivyal_client.dart';

enum AppEnvironment { dev, prod }

final Provider<AppEnvironment> appEnvironmentProvider = Provider<AppEnvironment>((ref) => kDebugMode ? AppEnvironment.dev : AppEnvironment.prod);

final Provider<String> serverUrlProvider = Provider<String>(
  (ref) => ref.watch(appEnvironmentProvider) == AppEnvironment.dev
      ? 'http://$localhost:8080/'
      : 'https://example.com/api/',
);
final Provider<String> serverWebServerUrlProvider = Provider<String>(
  (ref) => ref.watch(appEnvironmentProvider) == AppEnvironment.dev
      ? 'http://$localhost:8082/'
      : 'https://example.com/',
);

final Provider<Client> clientProvider = Provider<Client>(
  (ref) => Client(ref.watch(serverUrlProvider), authenticationKeyManager: FlutterAuthenticationKeyManager())..connectivityMonitor = FlutterConnectivityMonitor(),
);

final ChangeNotifierProvider<SessionManager> sessionManagerProvider =
    ChangeNotifierProvider<SessionManager>(
  (ref) {
    final sessionManager = SessionManager(caller: ref.watch(clientProvider).modules.auth);
    ref.onDispose(sessionManager.dispose);

    return sessionManager;
  },
);

```

Make sure to replace `https://example.com` with your Railway provided domain for the server

Notice that I moved the Serverpod client singleton to providers and added `authenticationKeyManager` to store authentication keys and tokens

### Login and Home screens

In `trivyal_flutter/lib/main.dart`

- Add a `ProviderScope` widget to the root of the app, and ensure the session manager is initialized. Then we can use the session manager to determine if the user is signed in or not and redirect to `LoginScreen` or `HomeScreen`

```dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:serverpod_auth_google_flutter/serverpod_auth_google_flutter.dart';

import 'auth/login_screen.dart';
import 'game_designer/home_screen.dart';
import 'utils/providers.dart';

void main() {
  runApp(const ProviderScope(child: TrivyalApp()));
}

class TrivyalApp extends ConsumerStatefulWidget {
  const TrivyalApp({super.key});

  @override
  ConsumerState<TrivyalApp> createState() => _TrivyalAppState();
}

class _TrivyalAppState extends ConsumerState<TrivyalApp> {
  late SessionManager sessionManager;
  Future<void>? initSessionFuture;

  @override
  Widget build(BuildContext context) {
    sessionManager = ref.watch(sessionManagerProvider);
    initSessionFuture ??= sessionManager.initialize();

    return FutureBuilder(
      future: initSessionFuture,
      builder: (context, sessionInitialized) {
        return MaterialApp(
          title: 'Trivyal',
          theme: ThemeData.from(
            colorScheme: ColorScheme.fromSeed(
              seedColor: Colors.deepPurple,
              brightness: PlatformDispatcher.instance.platformBrightness,
            ),
          ),
          home: sessionInitialized.connectionState == ConnectionState.done
              ? sessionManager.isSignedIn
                  ? const HomeScreen()
                  : const LoginScreen()
              : const Scaffold(body: Center(child: CircularProgressIndicator())),
        );
      },
    );
  }
}

```

- Now copy the `LoginScreen` code from [here](https://github.com/Andrew-Bekhiet/trivyal/blob/5fbb101c6e323cd28504148a7dbb2e1135cbbc94/trivyal_flutter/lib/auth/login_screen.dart) to `trivyal_flutter/lib/auth/login_screen.dart`. This will allow the user to either enter a game pin or sign in with google:
- Create `trivyal_flutter/lib/tils/secrets.dart` and add your Google Server Client Id that we created earlier:

```dart
const String googleServerClientId = '<Your google server client id>';
```

It should something like this `<random-numbers>-<random-text>.apps.googleusercontent.com` and should match the client id in `trivyal_server/config/google_client_secret.json`

<aside>

❕

Make sure to add `lib/utils/secrets.dart` to your `trivyal_flutter/.gitignore`

</aside>

To compile the app add a home screen at `trivyal_flutter/lib/game_designer/home_screen.dart`:

```dart
import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text('This is our home screen!')));
  }
}

```

### Storing Google client secret

We will use Railway environment variables to store google client secret

- Encode `google_client_secret.json` to base64 (It’s a best practice)

```bash
base64 -w 0 trivyal_server/config/google_client_secret.json
```

- Copy the output and paste it in a new variable in Railway `CONFIG_GOOGLE_CLIENT_SECRET`
- Open `Dockerfile` and add a step before starting the server to decode the secret to its destination:

```docker
...
EXPOSE 8082

ARG CONFIG_GOOGLE_CLIENT_SECRET
RUN echo $CONFIG_GOOGLE_CLIENT_SECRET | base64 -d > ./config/google_client_secret.json

ENTRYPOINT ./server --mode=$runmode --server-id=$serverid --logging=$logging --role=$role
```

- Encode `trivyal_flutter/lib/utils/secrets.dart` to base64

```bash
base64 -w 0 trivyal_flutter/lib/utils/secrets.dart
```

- Copy the output and paste it in a new variable in Railway `UTILS_SECRETS_DART`
- Add the decoding command in `Dockerfile` before the web build:

```docker
...
ARG UTILS_SECRETS_DART

RUN echo $UTILS_SECRETS_DART | base64 -d > ./lib/utils/secrets.dart

RUN flutter pub get
RUN flutter build web
...
```

### Deploying to Railway

Now let’s deploy to Railway!

- Run `dart bin/main.dart --mode production --apply-migrations` in `trivyal_server` dir to apply db migrations

![Don’t forget to run your database migration](https://cms.railway.com/media/66882177f033b7b131ccfa0c1a17808d55dd32308aafd603409429236e45ff4c.jpg)

- Deploy to Railway using `railway up -d`

## Adding Game designer 🎨

Let’s turn that empty home screen into games list.

### Adding models

This is how our database should be structured:

![Trivyal Database structure](https://cms.railway.com/media/fc88ad436fb6092f23cf9f74de7c0f7fb346481c08c378bdd3231702409867c3.png)

So let’s create our first model: **Game** (we already created **User** in the previous step)

- Create a new file `trivyal_server/lib/src/models/game.spy.yaml`

```yaml
class: Game
table: games
fields:
  name: String
  owner: module:auth:UserInfo?, relation(onDelete=cascade)
  questions: List<Question>?, relation(name=games_questions)
```

<aside>

ℹ️

You can learn more about working with relationships in Serverpod at [their documentation](https://docs.serverpod.dev/tutorials/first-app)

</aside>

- Add `trivyal_server/lib/src/models/question.spy.yaml`

```yaml
class: Question
table: questions
fields:
  gameId: int, relation(name=games_questions,parent=games, onDelete=cascade)
  text: String
  choices: List<Choice>
  correctChoiceId: int
  timeInSeconds: int
```

- Add `trivyal_server/lib/src/models/choice.spy.yaml`

```yaml
class: Choice
fields:
  id: int
  text: String
  color: int?
```

- Add `trivyal_server/lib/src/models/game_list_response.spy.yaml`

```yaml
class: GameListResponse
fields:
  data: List<Game>
```

- Run `serverpod generate` to generate dart models and database mappings
- Run `serverpod create-migration` to create db migration

### Adding CRUD Game endpoint

- Create a new file `trivyal_server/lib/src/endpoints/games_endpoint.dart`
- Copy the code from [here](https://github.com/Andrew-Bekhiet/trivyal/blob/master/trivyal_server/lib/src/endpoints/games_endpoint.dart) and paste it into the file you just created to add a `GamesEndpoint` class with the boring CRUD methods
- Run `serverpod generate` to update the generated code

### Edit game screen

Let’s change the home screen to show current user games using the `GamesEndpoint` we created in the previous step

- Add `material_symbols_icons` to dependencies
- Change `trivyal_flutter/lib/game_designer/home_screen.dart` to match [this file](https://github.com/Andrew-Bekhiet/trivyal/blob/5177645d8d7b0a04b7a10938239a62fe7ece458e/trivyal_flutter/lib/game_designer/home_screen.dart)
- Create a new file `trivyal_flutter/lib/game_designer/edit_game.dart`
- Copy [the code here](https://github.com/Andrew-Bekhiet/trivyal/blob/master/trivyal_flutter/lib/game_designer/edit_game.dart) and paste it into the file you just created to add an `EditGame` screen

This screen creates a new game if the game has null id, otherwise updates the game name

### Question editor widget

Now let’s add a question editor widget so we can add and edit questions

- Add `collection` and `flex_color_picker`to dependencies
- Create a new file `trivyal_flutter/ib/game_designer/question_widget.dart`
- Copy the code [here](https://github.com/Andrew-Bekhiet/trivyal/blob/master/trivyal_flutter/lib/game_designer/question_widget.dart) and paste it into the file you just created

If you run the server and the app locally, you should see something like this:

![edit-game-with-questions.gif](https://cms.railway.com/media/4f7f5bca712f2873fcc6e77ee8ee3b4436b16d53aa49b04726dad78bc041cf5b.gif)

## Hosting a live game 🌐

Let’s get the app setup to host live games.

### Live Games models

To save live game state, we’ll need to make a new table

- Create a new model `trivyal_server/lib/src/models/live_game.spy.yaml`

```yaml
class: LiveGame
table: live_games
fields:
  pin: int
  gameId: int, relation(parent=games)
  players: List<String>
  currentStatus: LiveGameStatus
  currentQuestion: Question?
  currentQuestionIndex: int?
  currentQuestionShowTime: DateTime?
  currentResults: Map<String, int>
  playerAnswers: Map<String, int>
  playerAnswersTime: Map<String, DateTime>
indexes:
  pin_idx:
    fields: pin
    unique: true
  gameId_idx:
    fields: gameId
    unique: true
```

- Add `trivyal_server/lib/src/models/live_game_status.spy.yaml`

```yaml
enum: LiveGameStatus
serialized: byName
values:
  - lobby
  - starting
  - answeringQuestion
  - revealingAnswer
  - podium
  - results
```

- Add `trivyal_server/lib/src/models/live_game_admin_event.spy.yaml` for sending admin events

```yaml
enum: LiveGameAdminEvent
serialized: byName
values:
  - startGame
  - showPodium
  - nextQuestion
```

- And `trivyal_server/lib/src/models/live_game_answer.spy.yaml` for players to send their answers

```yaml
class: LiveGameAnswer
fields:
  player: String
  answerId: int
```

- Run `serverpod generate && serverpod create-migration`

### Live Games Endpoint

Now we will create `LiveGames` endpoint which will handle game events in realtime and broadcast current state to all clients.

![LiveGames endpoint explained](https://cms.railway.com/media/26dac66c1025945de09a45b65825d99284a7741eb0ec66801a3b18eb9cfcecd4.png)

- Create a new endpoint `trivyal_server/lib/src/endpoints/live_games_endpoint.dart` and copy the code [https://github.com/Andrew-Bekhiet/trivyal/blob/master/trivyal_server/lib/src/endpoints/live_games_endpoint.dart](https://github.com/Andrew-Bekhiet/trivyal/blob/master/trivyal_server/lib/src/endpoints/live_games_endpoint.dart)

### Start button!

- Go to `trivyal_flutter/lib/game_designer/home_screen.dart` and add a start button to start hosting a live game!

```dart
          ...
          return ListView.builder(
              ...
              return ListTile(
                title: Text(game.name),
                onTap: () => Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) => EditGame(game: game)),
                ),
                trailing: IconButton(
                  icon: Icon(Symbols.play_circle),
                  tooltip: 'Start live game',
                  onPressed: () async {
                    final navigator = Navigator.of(context);

                    final streamController = StreamController<LiveGameAdminEvent>();

                    try {
                      final liveGameStream = client.liveGames.startOrJoinLiveGame(
                        gameId: game.id!,
                        adminMessages: streamController.stream,
                      );

                      // TODO: add game shell
                      // Navigator.push(
                      //   context,
                      //   MaterialPageRoute(
                      //     builder: (context) => GameShell(
                      //       liveGameStream: liveGameStream,
                      //       liveGamePlayerSink: streamController.sink,
                      //     ),
                      //   ),
                      // );
                    } catch (_) {
                      streamController.close();
                    }
                  },
                ),
              );
            },
          );
          ...
```

## Gameplay UI 🎮

Now we’ll setup the gameplay UI.

### Joining a Live Game

- Go to `trivyal_flutter/lib/auth/login_screen.dart` and add the enter button code:

```dart
...
FilledButton(
  child: const Text('Enter!'),
  onPressed: () {
    if (!Form.of(context).validate()) return;

    final scaffoldMessenger = ScaffoldMessenger.of(context);
    final streamController = StreamController<int>();
    try {
      final liveGameStream = client.liveGames.joinLiveGame(
        pin: int.parse(_pinController.text),
        playerName: _nameController.text,
        answerIdStream: streamController.stream,
      );

      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => GameShell(
            playerName: _nameController.text,
            liveGameStream: liveGameStream,
            liveGamePlayerSink: streamController.sink,
          ),
        ),
      );
    } catch (e) {
      streamController.close();

      if (e.toString().contains('Live game not found')) {
        scaffoldMessenger.showSnackBar(SnackBar(
          content: Text('Live game not found'),
          backgroundColor: Colors.red,
        ));
      }
    }
  },
);
...
```

### Game Shell screen

This screen will listen to the game stream and show a widget based on current state

- Create a new file `trivyal_flutter/lib/gameplay/game_shelll_screen.dart`
- Copy the code from [here](https://github.com/Andrew-Bekhiet/trivyal/blob/master/trivyal_flutter/lib/gameplay/game_shelll_screen.dart) and paste it into the file you just created

### Lobby widget

This is where players will be waiting until the admin presses start!

- Create a new file `trivyal_flutter/lib/gameplay/lobby_widget.dart` and add the following code

```dart
import 'package:flutter/material.dart';
import 'package:trivyal_client/trivyal_client.dart';

class LobbyWidget extends StatelessWidget {
  final LiveGame liveGame;

  const LobbyWidget({required this.liveGame, super.key});

  @override
  Widget build(BuildContext context) {
    if (liveGame.players.isEmpty) {
      return const Center(child: Text('Waiting for players to join the game...'));
    }

    return ListView.builder(
      itemCount: liveGame.players.length,
      itemBuilder: (context, index) => ListTile(title: Text(liveGame.players[index])),
    );
  }
}

```

### Live Game starting widget

Just a small 3s countdown animation

- Create a new file `trivyal_flutter/lib/gameplay/live_game_starting_widget.dart` and add the following code:

```dart
import 'package:flutter/material.dart';

class LiveGameStartingWidget extends StatefulWidget {
  const LiveGameStartingWidget({super.key});

  @override
  State<LiveGameStartingWidget> createState() => _LiveGameStartingWidgetState();
}

class _LiveGameStartingWidgetState extends State<LiveGameStartingWidget> with SingleTickerProviderStateMixin {
  late final AnimationController _animationController = AnimationController(vsync: this, duration: Duration(seconds: 3))..forward();

  late final Animation<int> countdownAnimation = _animationController.drive(
    TweenSequence([
      TweenSequenceItem(tween: ConstantTween(3), weight: 1),
      TweenSequenceItem(tween: ConstantTween(2), weight: 1),
      TweenSequenceItem(tween: ConstantTween(1), weight: 1),
    ]),
  );

  late final Animation<double> scaleAnimation = _animationController
      .drive(
        TweenSequence<double>([
          TweenSequenceItem(tween: Tween(begin: 1, end: 0), weight: 1),
          TweenSequenceItem(tween: Tween(begin: 1, end: 0), weight: 1),
          TweenSequenceItem(tween: Tween(begin: 1, end: 0), weight: 1),
        ]),
      ).drive(CurveTween(curve: Curves.fastOutSlowIn));

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: _animationController,
        builder: (context, child) {
          return Transform.scale(
            scale: scaleAnimation.value,
            child: Text(
              countdownAnimation.value.toString(),
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 80),
            ),
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }
}
```

### Answering Questions widget

The actual question and answers, and the correct answer when answers are revealed

- Create a new file `trivyal_flutter/lib/gameplay/answering_questions_widget.dart`
- Copy the code from [here](https://github.com/Andrew-Bekhiet/trivyal/blob/master/trivyal_flutter/lib/gameplay/answering_questions_widget.dart) and paste it into the file you just created

### Podium Widget

Animates players’ scores and their ranks so far

- Create a new file `trivyal_flutter/lib/gameplay/podium_widget.dart`
- Copy the code from [here](https://github.com/Andrew-Bekhiet/trivyal/blob/master/trivyal_flutter/lib/gameplay/podium_widget.dart) and paste it into the file you just created

### Rank widget

Shows the final rank of each player on their screen

- Create `trivyal_flutter/lib/gameplay/rank_widget.dart` and add the following code:

```dart
import 'package:flutter/material.dart';

class RankWidget extends StatelessWidget {
  final int rank;

  const RankWidget({super.key, required this.rank});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            'You are #',
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.bodyLarge,
          ),
          Text(
            rank.toString(),
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.displayLarge,
          ),
        ],
      ),
    );
  }
}
```

## Putting it all together ✨

After finishing the endpoint and the UI code, we’re ready to `railway up -d` and play!

![trivyal-demo.gif](https://cms.railway.com/media/5c5fa3c52525c6bcbdaee3d4fb659d91d9a4a165b5da9b3f833a6aad688c6cc5.gif)

And that was how to deploy a Flutter/Dart app to Railway!

I hope you enjoyed reading the article as much as I enjoyed writing it!

## Useful resources 📖

Here are some helpful resources if you have any trouble.

- [Railway Trivyal Template](https://railway.com/new/template/4fD3YO?referralCode=androidquartz)
- [GitHub repo](https://github.com/Andrew-Bekhiet/trivyal)
- [Railway Docs](https://docs.railway.com/)
- [Serverpod Docs](https://docs.serverpod.dev/)