This is part two of our social gaming series, in which we walk through key features of our Daily-powered social game: Code of Daily: Modern Wordfare. This is a standalone post. You do not need to read the rest of the series to follow along. But if you’re curious about the greater context of our application, check out part one.

Introduction

At Daily, we make it simple to embed interactive video calls into a client-side application. This client-only approach works really well for many use cases, especially if you don’t need to create rooms or retrieve meeting tokens on- demand.

But some applications get a bit more complicated. For example, in our social game, players are able to not just join existing games from the game lobby, but create new ones as well. And each game needs its own room, so we’ll need to generate them at runtime.

In this post, we will go over how we implemented this in Code of Daily: Modern Wordfare, focusing on our lobby flows and room creation via Daily’s REST API. Specifically, we will use Daily's /rooms endpoint to generate video call rooms.

Getting started

If you'd like to follow along with the code, check out the instructions to run the game locally in our introduction to Code of Daily (CoD). You can also follow along in the code repository.

How does Daily's REST API work?

Daily's REST API works by accepting requests with an Authorization header containing a Daily API key. The API key can be found in the Developers section of your Daily dashboard. Keep it secret. Keep it safe.

Gandalf telling Frodo to "Keep it secret. Keep it safe."

How do you keep it safe, though?


Distribute your API key via secure channels

Avoid distributing your API key in messaging platforms like Slack or email. Password managers like 1Password provide more secure ways of sharing credentials with teammates, negating the need to paste around API keys. Channels like Slack are generally not safe sources for secret sharing — just ask Electronic Arts.

Avoid submitting plaintext API keys in version control

It can be tempting to commit your API keys into version control if your repository is private — nobody's going to see it anyway, right?

According to GitGuardian's announcement of their 2022 report on secrets sprawl, "When compared to open-source corporate repositories, private ones are also four times more likely to expose a secret."

If you need to make secrets accessible in GitHub (such as for GitHub Actions to consume), consider GitHub's encrypted secrets.

Do not expose your API key to clients

Clients like web browsers can't really be trusted to keep a secret secret. This is why API keys are best kept out of their reach.

Often (such as in the case of our social game), this means keeping API keys on a server instead. This can be a dedicated server deployment or something stateless, like AWS Lambda functions. Another example of keeping an API key server-side is using Netlify's file-based configuration to set up a redirect to an API endpoint. The redirect can then have the API key set as an environment variable which is added to the Authorization header as part of the redirect configuration, without being exposed to the client.

In this post, we'll go through the standalone server approach, which we're using for our Daily-powered social game.

Keeping API keys on a secure server and having that server make requests to Daily's REST API allows you to create Daily WebRTC rooms without exposing your API key to the world.

If your Daily API key is compromised…

If you’ve accidentally done one of the above or think your Daily API key may be otherwise compromised, you can regenerate the key from the Daily dashboard. Click on the More menu on the right hand side of your API key and click “Regenerate key…”

❗Remember that once you regenerate your Daily API key, any existing usages of your old key will cease to function. Be prepared to update them right away.
Daily API key regeneration UI
Regenerating the Daily API key

Now that we know a bit about how to safely use your API key, let’s dig into the code.

Setting up our app server

In our server index.ts file, one of the first things we will do is import our DAILY_API_KEY env variable:

import { DAILY_API_KEY } from  "./env";

This variable is set in our local .env file, which is not submitted to version control. If you're curious, you can check out how we configure webpack to propagate variables from .env to our environment in our webpack configuration file.

Before doing anything else, we'll verify that the DAILY_API_KEY variable actually exists, and throw an error if not. There is no point starting the server at all if the API key is not provided, as it would make creating actual games impossible in our case.

// Fail early if the server is not appropriately configured.
if (!DAILY_API_KEY) {
  throw new Error(
    "failed to start server: Daily API key missing from configuration. Please check your .env file."
  );
}

If the Daily API key is set, we'll go ahead and start the server:

function startServer() {
  // Init our Express app
  const app = express();
  // Init game orchestrator
  const orchestrator = new GameOrchestrator(new Memory());
  const port = PORT || 3001;

  // Return path to our client-side assets
  function getClientPath(): string {
    const basePath = dirname(__dirname);
    return join(basePath, "client");
  }

  const clientPath = getClientPath();

  // Make client-side assets reachable from root
  // or from `/client` subdirectory.
  app.use(express.static(clientPath));
  app.use("/client", express.static(clientPath));
  // Parse JSON in request bodies
  app.use(express.json());
  // ... our handlers are defined below
}

Above, we start with initializing our Express application and our game orchestrator. The orchestrator is responsible for actually creating, storing, and manipulating our game sessions. This includes creating Daily rooms for each game!

We then set up our Express app to serve static files in our client directory, and to automatically parse JSON bodies from incoming requests. Now, we're ready to add our game creation and join handlers.

Game and Daily room creation

The handler that receives POST requests to our server /create endpoint creates our CoD: Modern Wordfare game. Each game has its own Daily room. We've omitted some request validation logic for brevity, but feel free to check it out in full in our demo repository:

  // /create endpoint handles creating a game
  app.post("/create", (req: Request, res: Response) => {
    const body = <CreateGameRequest>req.body;
    const { wordSet, gameName } = body;
    
	// Create the game - this is where Daily room creation happens! 
    orchestrator
      .createGame(gameName, wordSet)
      .then((game) => {
        // Retrieve meeting token from Daily REST API. 
        // We'll go through meeting tokens in a separate post.
        orchestrator
          .getMeetingToken(game.dailyRoomName)
          .then((token) => {
            // Set meeting token for this game as a cookie
            const cookie = getCookieVal(token, game.id);
            res.cookie(meetingTokenCookieName, cookie);
            res.redirect(`/?gameID=${game.id}&playerName=${body.playerName}`);
          })
          .catch((error) => {
            console.error("failed to get meeting token", error);
            res.sendStatus(500);
          });
      })
      .catch((error) => {
        console.error("failed to create room:", error);
        res.sendStatus(500);
      });
  });

Above, we retrieve relevant data (like the word set and the game name) from the game creation request. We then pass these to the game orchestrator, instructing it to create the actual game for us. If successful, we redirect the client to our game join URL with relevant game and player data pre-populated in the query string.

Now, let's see exactly how Daily's REST API is actually used to create the Daily room.

Room creation with Daily's REST API

createGame() is where Daily's REST API is invoked to generate a room using the /rooms endpoint:

  // createGame() creates a new game using the given name and word set.
  // It does so by creating a Daily room and then an instance of Game.
  async createGame(name: string, wordSet: Word[]): Promise<Game> {
    const apiKey = DAILY_API_KEY;

    // Prepare our desired room properties. Participants will start with
    // mics and cams off, and the room will expire in 24 hours.
    const req = {
      properties: {
        exp: Math.floor(Date.now() / 1000) + 86400,
        start_audio_off: true,
        start_video_off: true,
      },
    };

    // Prepare our headers, containing our Daily API key
    const headers = {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    };

    const url = `${dailyAPIURL}/rooms/`;
    const data = JSON.stringify(req);
    // Make the actual POST request to Daily:
    const res = await axios.post(url, data, { headers }).catch((error) => {
      console.log("failed to create room:", res);
      throw new Error(`failed to create room: ${error})`);
    });

    if (res.status !== 200 || !res.data) {
      console.error("unexpected room creation response:", res);
      throw new Error("failed to create room");
    }
    const body = JSON.parse(JSON.stringify(res.data));

    // Cast Daily's response to our room data interface.
    const roomData = <CreatedDailyRoomData>body;

    // Instantiate game with given room URL and name, and store
    // the newly created game.
    const game = new Game(name, roomData.url, roomData.name, wordSet);
    await this.storeClient.storeGame(game);
    return game;
  }

The above function configures and executes a POST request to Daily's  /rooms endpoint.

This endpoint takes some room properties, and requires an Authorization header that includes our Daily API key (which as you'll remember is set in the local .env file).

We then check for any errors, either in the request itself or in the response from Daily. If Daily returns a 200 status code, we know our room creation has been successful. You should be able to see your new room in your Daily dashboard at this point.

If successful, Daily's response body includes information about the new room, such as its exact URL. We can now use this data as needed for the rest of our application logic. In the case of our social game, we go on to create a new game with the data and store it on the server.

From here, users can join our game (and associated Daily room) by making a POST request to our /join endpoint. Our application server's join handler retrieves the game data from storage, which includes information about the relevant Daily room. Once retrieved, the server sends this game and room information back to the client. The client can then use daily-js to join our video call as part of the game session. The join process does not involve any further interaction with the Daily API.

Invoking our room creation handler from the client

On the client side, we create two forms in our index.html file:

        <form id="join-game-form" class="invisible">
          <h2>Join a game</h2>
          <input
            type="text"
            placeholder="Your Name"
            id="join-player-name"
            required
          />
          <button type="submit" id="join-game">Join game</button>
        </form>
        <form id="create-game-form" class="invisible">
          <h3>Start a new game</h3>
          <input type="text" placeholder="Game Name" id="game-name" required />
          <input
            type="text"
            placeholder="Your Name"
            id="create-player-name"
            required
          />
          <button type="submit" id="create-game">Create game</button>
        </form>

One form is used for game join and only takes a player name. The other is used for game creation and requires a player name and a game name.

Form to join a Code of Daily: Modern Wordfare game
Game join form
Form to create a Code of Daily: Modern Wordfare game
Game creation form

Now, we just need to figure out which form to show. We do so in our index.ts file:

window.addEventListener("DOMContentLoaded", () => {
  // See if we have any query parameters indicating the user
  // is joining an existing game
  const usp = new URLSearchParams(window.location.search);
  const params = Object.fromEntries(usp.entries());

  // If a game ID was specified in the URL parameters,
  // start the game join process. Otherwise, start
  // game creation process.
  if (params.gameID) {
    initJoinProcess(params);
    return;
  }
  initCreateProcess();
});

Above, when the user visits our game's home page and the DOM is loaded, we check the query parameters for the presence of a game ID. If one exists, we take the user through the game join flow. Otherwise, we take them through the game creation flow. Let's go through the creation flow in a little more detail.

Client-side game/room creation flow

The initCreateProcess() function shows our game creation form and defines a submission handler for it. We won't go through these parts in detail in this post. Instead, let's focus on the final call we make in this function when the creation form is submitted: createGame():

// createGame() makes a POST request to the /create
// endpoint to make a new game.
async function createGame(
  gameName: string,
  playerName: string,
  wordSet: Word[]
) {
  const reqData = <CreateGameRequest>{
    gameName,
    playerName,
    wordSet,
  };

  const headers = {
    "Content-Type": "application/json",
  };

  const url = "/create";
  const data = JSON.stringify(reqData);

  const req = <RequestInit>{
    method: "POST",
    body: data,
    redirect: "follow",
    headers,
  };

  // The call that actually POSTs to our server
  await fetch(url, req)
    .then((res) => {
      // If the game has been created successfully,
      // the server responds with a redirect, which we
      // follow.
      window.location.assign(res.url);
    })
    .catch((error) => {
      throw new Error(`failed to create game: ${error})`);
    });
}

Above, we prepare a POST request with the data our server expects for its game creation handler:

  • Game name
  • Player name
  • Word set

We then make a POST request to our /create endpoint. At which point the server-side game creation handler we covered earlier takes over and generates the game and Daily room for us.

That's it! Our Daily room and the game session it's owned by have been created on the server, without exposing our domain's Daily API key to the client.

Wrapping up

In this post, we went through a basic flow of creating Daily rooms on demand at runtime in an application. We did so without exposing our Daily API key to the client, and made relevant room data available to the client to enable them (and others) to be able to join the new video call.

We hope this overview shed some light into what room creation with Daily's REST API can look like. Please reach out if you have any questions or other use cases you might like some help with!

In the next post of our social gaming series we'll talk about WebSockets. We'll show how we utilize them in Code of Daily: Modern Wordfare and discuss a client-side alternative which Daily provides to send data between participants.