> ## Documentation Index
> Fetch the complete documentation index at: https://docs.playflowcloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# PlayFab Bridge

> Keep PlayFab for matchmaking and identity while hosting your dedicated game servers on PlayFlow.

This guide shows you how to automatically deploy a PlayFlow game server when players are matched through **PlayFab's matchmaker**, and how clients discover the server through **PlayFab Player Data** — which the server itself updates once it is ready.

It's the same pattern popularized by other hosting providers, mapped onto PlayFlow's API. If you're already invested in PlayFab for matchmaking and identity, this lets you adopt PlayFlow for server hosting without moving off PlayFab.

<Info>
  **PlayFlow also has a native [lobby and matchmaking system](/lobbies/overview).** If you're starting fresh, that path is simpler — no PlayFab required. Use this bridge when you specifically want to keep PlayFab's matchmaker or player identity.
</Info>

Before getting started, we expect that:

* You currently use PlayFab (a title, a matchmaking queue, and some registered players).
* You have a PlayFlow project and a **dedicated Linux server build** ready to upload. See the [Docker builds guide](/guides/docker-builds) or the [Unity quickstart](/quickstart/fishnet) if you don't yet.

## How it works

<Steps>
  <Step title="PlayFab matches players">
    PlayFab's matchmaker fires a `playfab.matchmaking.match_found` event.
  </Step>

  <Step title="A CloudScript rule calls PlayFlow">
    A PlayFab Automation rule runs a CloudScript function that calls `POST /api/v3/servers/start`, passing the PlayFab match ID, title secret, and player list as environment variables.
  </Step>

  <Step title="PlayFlow launches the server">
    PlayFlow allocates the network ports immediately and boots your build. It injects everything the server needs as environment variables (`PLAYFLOW_PORTS`, your custom vars, and API credentials).
  </Step>

  <Step title="The server publishes its address">
    On startup, the server reads its own public `host:port` from `PLAYFLOW_PORTS` and writes it into each matched player's PlayFab Player Data — keyed by the match ID.
  </Step>

  <Step title="Clients connect">
    Each client polls its PlayFab ticket until matched, then reads the connection info from Player Data and connects. Because the server only writes Player Data once it's actually running, clients never get a stale address.
  </Step>
</Steps>

```mermaid theme={null}
sequenceDiagram
    participant C as Game Client
    participant PF as PlayFab
    participant CS as PlayFab CloudScript
    participant PL as PlayFlow API
    participant S as Game Server

    C->>PF: CreateMatchmakingTicket
    PF-->>PF: match_found event
    PF->>CS: Automation rule → StartPlayFlowDeployment
    CS->>PL: POST /api/v3/servers/start (env: match id, secret, players)
    PL->>S: Boot build, inject PLAYFLOW_PORTS + env
    S->>PF: UpdateUserData → "host:port" (keyed by match id)
    C->>PF: Poll ticket → Matched → GetUserData
    PF-->>C: host:port
    C->>S: Connect
```

## Step 1 — Prepare your PlayFlow project

<Steps>
  <Step title="Get a server API key">
    In your PlayFlow dashboard, open your project's **API Keys** and copy a **server key** (`pf_...`). This key can start servers and must stay secret — treat it exactly like your PlayFab Title Secret. Never ship it in a client build.
  </Step>

  <Step title="Upload your server build">
    Upload your dedicated Linux server build and give it a **name** (for example `space-edge`). PlayFlow references builds by this name — the value becomes the `version_tag` you pass when starting a server, and each upload under the same name auto-increments its version. See [Docker builds](/guides/docker-builds) for the upload flow.
  </Step>

  <Step title="Configure a network port">
    In your project settings, add a port config and give it a **name** you'll look up later — this guide uses `game`. Choose `UDP` for most game netcode (for example FishNet's Tugboat transport), or `TCP` (optionally with TLS) for WebSocket-based clients. PlayFlow allocates the public port at launch; your server reads it back from `PLAYFLOW_PORTS`.
  </Step>
</Steps>

## Step 2 — Create the PlayFab CloudScript function

Log in to PlayFab, open your title, and under **Automation → Revisions (Legacy)** deploy a new revision containing the function below. It calls PlayFlow's start endpoint and forwards everything the server will need.

Update the four variables at the top with your own values first. The `PlayFlow_BuildName` must match the build name you uploaded in Step 1.

<Warning>
  Keep this function server-side. It holds your PlayFlow **server key** and PlayFab **Title Secret** — the CloudScript is the trusted interface that keeps both hidden from players.
</Warning>

```javascript theme={null}
var PlayFlow_ApiUrl     = "https://api.computeflow.cloud/api";
var PlayFlow_ApiKey     = "pf_your_server_key";   // PlayFlow server key — keep secret
var PlayFlow_BuildName  = "space-edge";           // the build NAME you uploaded
var PlayFab_TitleSecret = "TITLE_SECRET";

// PlayFlow regions with approximate coordinates, for nearest-region selection.
// Full list: https://docs.playflowcloud.com/fundamentals/regions
var PlayFlow_Regions = [
  { region: "us-east",              lat: 40.7,  lon: -74.0 },
  { region: "us-south",             lat: 32.8,  lon: -96.8 },
  { region: "us-west",              lat: 34.0,  lon: -118.2 },
  { region: "eu-west",              lat: 50.1,  lon: 8.7 },
  { region: "eu-north",             lat: 59.3,  lon: 18.1 },
  { region: "eu-uk",                lat: 51.5,  lon: -0.1 },
  { region: "ap-south",             lat: 19.1,  lon: 72.9 },
  { region: "sea",                  lat: 1.35,  lon: 103.8 },
  { region: "ap-north",             lat: 35.7,  lon: 140.4 },
  { region: "ap-southeast",         lat: -33.9, lon: 151.2 },
  { region: "south-america-brazil", lat: -23.5, lon: -46.6 },
  { region: "south-africa",         lat: -26.2, lon: 28.0 },
];

function nearestRegion(points) {
  // Average the players' locations, then pick the closest PlayFlow region.
  if (!points.length) return "us-east";
  var avgLat = points.reduce(function (s, p) { return s + p.lat; }, 0) / points.length;
  var avgLon = points.reduce(function (s, p) { return s + p.lon; }, 0) / points.length;
  var best = null, bestD = Infinity;
  PlayFlow_Regions.forEach(function (r) {
    var d = Math.pow(r.lat - avgLat, 2) + Math.pow(r.lon - avgLon, 2);
    if (d < bestD) { bestD = d; best = r.region; }
  });
  return best;
}

handlers.StartPlayFlowDeployment = function (args, context) {
  var QueueName = context.playStreamEvent.Payload.QueueName;
  var MatchId   = context.playStreamEvent.Payload.MatchId;

  var members = multiplayer.GetMatch({
    QueueName: QueueName,
    MatchId: MatchId,
    EscapeObject: false,
    ReturnMemberAttributes: false,
  }).Members;

  var locations = [];
  var playerIds = [];

  members.forEach(function (member) {
    var profile = entity.GetProfile({ Entity: member.Entity });
    var masterId = profile.Profile.Lineage.MasterPlayerAccountId;

    var loc = server.GetPlayerProfile({
      PlayFabId: masterId,
      ProfileConstraints: { ShowLocations: true },
    });

    var l = loc.PlayerProfile.Locations && loc.PlayerProfile.Locations[0];
    if (l) locations.push({ lat: l.Latitude, lon: l.Longitude });
    playerIds.push(masterId);
  });

  var region = nearestRegion(locations);

  var response = JSON.parse(
    http.request(
      PlayFlow_ApiUrl + "/v3/servers/start",
      "post",
      JSON.stringify({
        name: "match-" + MatchId.slice(0, 20),
        region: region,
        version_tag: PlayFlow_BuildName,
        environment_variables: {
          PLAYFAB_MATCH_ID: MatchId,
          PLAYFAB_TITLE_SECRET: PlayFab_TitleSecret,
          PLAYFAB_MATCH_PLAYERS_ID_LIST: playerIds.join(","),
        },
      }),
      "application/json",
      { "api-key": PlayFlow_ApiKey }
    )
  );

  log.info("PlayFlow server starting", response);
};
```

<Info>
  PlayFlow returns the `instance_id`, a `status` of `launching`, and the allocated `network_ports` **synchronously** from this call — the ports are reserved before the machine boots. This guide has the *server* publish the address (Step 4) so that connection info appears in Player Data exactly when the server is ready. If you'd rather publish from CloudScript, you can instead poll `GET /api/v3/servers/{instance_id}` until `status` is `running` and read `network_ports` from the response.
</Info>

## Step 3 — Add the automation rule

Under **Automation → Rules**, create a new rule:

* **Event Type**: `playfab.matchmaking.match_found`
* **Action** → **Type**: `Execute Entity CloudScript`
* **CloudScript Function**: `StartPlayFlowDeployment`

Now every completed match automatically launches a dedicated PlayFlow server.

## Step 4 — Publish the address from the server

When PlayFlow starts your server, it injects these environment variables into the container (alongside any `environment_variables` you passed from CloudScript):

| Variable                                | Description                                                                                                                  |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `PLAYFLOW_PORTS`                        | JSON array of the server's public ports: `[{ "name", "internal_port", "external_port", "protocol", "host", "tls_enabled" }]` |
| `PLAYFLOW_INSTANCE_ID`                  | This server's instance ID                                                                                                    |
| `PLAYFLOW_API_KEY` / `PLAYFLOW_API_URL` | Credentials + base URL to call the PlayFlow API back if needed                                                               |
| `PLAYFLOW_REGION`                       | The region the server launched in                                                                                            |
| Your custom vars                        | `PLAYFAB_MATCH_ID`, `PLAYFAB_TITLE_SECRET`, `PLAYFAB_MATCH_PLAYERS_ID_LIST`                                                  |

On startup, read the public address for your named port out of `PLAYFLOW_PORTS`, then write `host:external_port` into each matched player's Player Data, keyed by the match ID.

```csharp PlayerDataUpdateService.cs theme={null}
public class PlayerDataUpdateService : NetworkBehaviour
{
    [SerializeField] private string ServerPortName = "game"; // must match your PlayFlow port config name

    private PlayfabRestApiService _playfab;

    public override void OnStartServer()
    {
        _playfab = FindObjectOfType<PlayfabRestApiService>();

        string matchId      = Environment.GetEnvironmentVariable("PLAYFAB_MATCH_ID");
        string titleSecret  = Environment.GetEnvironmentVariable("PLAYFAB_TITLE_SECRET");
        string[] playerIds  = Environment.GetEnvironmentVariable("PLAYFAB_MATCH_PLAYERS_ID_LIST")?.Split(',');
        string portsJson    = Environment.GetEnvironmentVariable("PLAYFLOW_PORTS");

        if (string.IsNullOrEmpty(portsJson))
        {
            Debug.LogError("PLAYFLOW_PORTS not set — is this running on PlayFlow?");
            return;
        }

        // PLAYFLOW_PORTS is a JSON array; find the port config by name.
        PlayFlowPort[] ports = JsonConvert.DeserializeObject<PlayFlowPort[]>(portsJson);
        PlayFlowPort game = Array.Find(ports, p => p.name == ServerPortName);
        if (game == null)
        {
            Debug.LogError($"No PlayFlow port named '{ServerPortName}'");
            return;
        }

        string connection = $"{game.host}:{game.external_port}";

        foreach (string playerId in playerIds)
        {
            if (!string.IsNullOrEmpty(playerId))
                _playfab.UpdatePlayerData(titleSecret, playerId, matchId, connection);
        }
    }

    [Serializable]
    private class PlayFlowPort
    {
        public string name;
        public int external_port;
        public string host;
        public string protocol;
        public bool tls_enabled;
    }
}
```

The PlayFab write itself is unchanged from any PlayFab server integration — authenticate with the Title Secret and call `Server/UpdateUserData`:

```csharp PlayfabRestApiService.cs (server) theme={null}
public async void UpdatePlayerData(string secretKey, string playerId, string key, string value)
{
    if (!_http.DefaultRequestHeaders.Contains("X-SecretKey"))
        _http.DefaultRequestHeaders.Add("X-SecretKey", secretKey);

    var data = new JObject { [key] = value };
    var body = new JObject { ["PlayFabId"] = playerId, ["Data"] = data };

    var content  = new StringContent(body.ToString(), Encoding.UTF8, "application/json");
    var response = await _http.PostAsync($"https://{TitleID}.playfabapi.com/Server/UpdateUserData", content);

    if (!response.IsSuccessStatusCode)
        Debug.Log($"Could not update Player Data for {playerId}: {(int)response.StatusCode}");
}
```

## Step 5 — Connect from the client

The client side is **pure PlayFab** and identical to any PlayFab matchmaking integration — PlayFlow isn't involved until the client reads the address. The flow:

<Steps>
  <Step title="Log in">
    Authenticate the player with PlayFab (for example `LoginWithCustomID`) and store the entity + session tokens.
  </Step>

  <Step title="Create a ticket">
    Call `Match/CreateMatchmakingTicket` on your queue and keep the returned ticket ID.
  </Step>

  <Step title="Wait for a match">
    Poll `Match/GetMatchmakingTicket` every few seconds until `Status` is `Matched`, then store the `MatchId`.
  </Step>

  <Step title="Read the address & connect">
    Poll `Client/GetUserData` with the `MatchId` as the key until the server has written it, then set your transport's address/port and connect. Retry for a few seconds — the server writes Player Data the moment it boots, which is typically a few seconds after the match.
  </Step>
</Steps>

```csharp Reading the address and connecting (client) theme={null}
private async void StartConnectionAttempt()
{
    var body = new JObject { ["Keys"] = new JArray { _matchId } };
    var content = new StringContent(body.ToString(), Encoding.UTF8, "application/json");

    while (true)
    {
        var res  = await _http.PostAsync($"https://{TitleID}.playfabapi.com/Client/GetUserData", content);
        var json = JObject.Parse(await res.Content.ReadAsStringAsync());
        var data = json["data"]?["Data"];

        var entry = data?[_matchId]?["Value"]?.ToString();
        if (!string.IsNullOrEmpty(entry))
        {
            string address = entry.Split(':')[0];
            if (ushort.TryParse(entry.Split(':')[1], out ushort port))
            {
                InstanceFinder.TransportManager.Transport.SetClientAddress(address);
                InstanceFinder.TransportManager.Transport.SetPort(port);
                InstanceFinder.ClientManager.StartConnection();
            }
            return;
        }

        await Task.Delay(3000); // server not ready yet — retry
    }
}
```

<Tip>
  Have the client remove its match entry from Player Data once connected (`Client/UpdateUserData` with `KeysToRemove`) so stale connection info doesn't accumulate.
</Tip>

## Step 6 — Build, upload, and test

<Steps>
  <Step title="Build a headless Linux server">
    Produce a dedicated Linux server build with your netcode configured to start on headless and listen on the internal port that matches your PlayFlow port config.
  </Step>

  <Step title="Upload it to PlayFlow">
    Upload the build under the same **name** you referenced as `PlayFlow_BuildName` in the CloudScript (`space-edge` here). See [Docker builds](/guides/docker-builds).
  </Step>

  <Step title="Run two clients">
    Launch two PlayFab-authenticated clients and start matchmaking on both. When PlayFab matches them, your rule fires, PlayFlow boots the server, the server publishes its address, and both clients connect to the same deployment.
  </Step>
</Steps>

## Why this works well on PlayFlow

* **The server is handed its own public address.** `PLAYFLOW_PORTS` contains the resolved `host` and `external_port` for every named port — no self-lookup or metadata call required.
* **Readiness is automatic.** Because the server writes Player Data in `OnStartServer`, connection info only appears once the process is actually up. Clients that poll can't grab a dead address.
* **The server can talk back to PlayFlow.** `PLAYFLOW_API_KEY` and `PLAYFLOW_API_URL` are injected too, so the server can stop itself when the match ends (`DELETE /api/v3/servers/{instance_id}`) or update its own metadata — no extra secrets to manage.
* **Set a TTL as a safety net.** Pass a `ttl` (seconds) on `POST /servers/start` so an abandoned match can't leave a server running forever. See the [API reference](/fundamentals/api).

<Info>
  When you're ready to simplify further, PlayFlow's [native matchmaking](/lobbies/matchmaking) removes the PlayFab round-trip entirely — the matchmaker launches the server and hands the connection details straight to your players over a real-time channel.
</Info>
