Metaplay logo
Metaplay AI
DemoPricing

Migrating from PlayFab to Metaplay: The Complete Technical Guide

Petri Kero & Antti Hätälä
June 12, 2026comparisons

A complete technical guide to migrating from PlayFab to Metaplay, built from real migration projects: architecture, timeline, common gotchas, and a full subsystem-by-subsystem reference.

Migrating from PlayFab to Metaplay is a well-trodden path. We've worked through several complex PlayFab migration projects, and the same questions, patterns, and gotchas come up every time: what replaces Azure Functions, what happens to the Unity client, how long does it take, and what can go wrong.

This guide is the answer to those questions in one place. It's adapted from the technical migration reports we produce for studios evaluating the move — anonymised and generalised so it applies to any team running a live game on PlayFab. The shapes described here aren't hypothetical: every pattern, table, and estimate comes from real codebases we've migrated.

A year ago, a migration like this took months of careful engineering. The tooling available now — and the advances we've made in the Metaplay SDK — have dramatically reduced the time and effort it takes to migrate even the most established games.

How to use this guide

The guide is split into three parts:

  • Part 1: Migration overview — Why studios migrate, what the concrete benefits are, what determines the timeline, and which decisions to surface early. Written to align technical and non-technical stakeholders.
  • Part 2: The technical plan — The engineering detail: the architectural shift, a worked example feature, determinism rules, the client refactor, the deletion catalog, common gotchas, testing, and the risk register. Aimed at the engineers planning or executing the migration.
  • Part 3: Subsystem-by-subsystem reference — A self-contained entry for each major subsystem in a typical PlayFab game: auth, save data, game definitions, RPC endpoints, mail, IAP, guilds, PvP, segments, analytics, LiveOps, push, and cheat endpoints. Read the ones relevant to your project and skip the rest.

It's long and detailed by design. You'll probably get the most out of it by pointing your AI coding assistant at it and asking questions specific to your codebase — and you can connect your coding harness to the Metaplay docs MCP server for answers grounded in even more Metaplay context. See Metaplay AI for the full AI tooling picture.

Part 1: Migration overview

A high-level overview of why and how to migrate to Metaplay from PlayFab. This part aligns technical and non-technical stakeholders on the goals, benefits, and key decisions of a migration project.

Executive summary

Many studios on PlayFab reach a point where the backend gets in the way of running the game. A typical PlayFab setup — PlayFab for player state and design data, a serverless function host for game logic, a separate push service, and bespoke glue holding it together — was the practical choice when the game launched. As the game matures, the cost of operating that setup grows. Engineering time goes to keeping the pieces stitched together rather than to building features. Players wait for a server response on nearly every action they take. The team that runs the game in market cannot run broadcasts, segment campaigns, experiments, or routine customer support without involving engineering.

By moving to Metaplay, the studio gains a single platform built for operating a live game. Live-operations work happens on a dashboard, not in a code repository. Players feel a faster game because the client predicts the outcome of actions and the server confirms it. Customer support handles tickets through built-in tooling instead of an engineer running a script. Engineering spends its time on game features instead of backend plumbing.

Why migrating off PlayFab is worth doing

Moving to Metaplay addresses pain points that show up across PlayFab projects:

  • Faster live operations – The team running the game gains a unified dashboard for broadcasts, mail campaigns, segmented offers, A/B experiments, and runtime tuning. Work that requires an engineer today moves to the people doing it.
  • A more responsive game – Player actions land in the UI immediately because the client predicts the outcome and the server confirms it. Today's per-action server round-trip disappears from the player's experience.
  • Customer support that scales – Built-in support tooling — player search, mail and item grants, refund flows, gated admin actions — replaces today's pattern of an engineer running a script for every CS request.
  • Leverage across the studio's portfolio – Once one title runs on Metaplay, the next title starts from the same backend. Backend investment amortizes across games rather than being rebuilt per project.

Together these outcomes let the studio operate a bigger live game with fewer engineering workarounds, and free the team to spend its time on what the players actually see.

What stays in place during the migration

The migration is limited to the backend. Several parts of the stack stay exactly where they are today:

  • Unity gameplay layer – Rendering, animation, audio, and input stay untouched.
  • Adjacent integrations – Wwise, Addressables, and Unity Purchasing keep their integrations.
  • Multiplayer engine – Continues to handle combat and matchmaking.
  • Game design data – Definitions, progression curves, and balance keep the same structure even though the storage format and integration layer change.
  • Content authoring – Day-to-day workflow for designers is preserved.

What changes is the operational backend behind those layers; what the players touch and what the team has invested in adjacent integrations stays put.

Key benefits of moving from PlayFab to Metaplay

Metaplay eliminates whole categories of code that exist in a typical PlayFab and Azure stack solely because of platform constraints, and unlocks capabilities that the source backend does not offer at all. The subsections below name each gain concretely and contrast it against the shape teams on PlayFab end up living with.

One vertically integrated platform

A typical PlayFab backend is a federation: PlayFab for player state, design data, auth, and segments; a serverless function host for authoritative logic; a separate maintenance-flag service; a third-party push provider; a custom log-bridging app for observability; and bespoke orchestration for the mail pipeline. Each piece carries its own data model, dashboard, credentials, failure mode, and upgrade cadence; the integrations between them are glue.

Metaplay collapses the federation into one platform. Game logic, player state, design data, broadcasts, segments, experiments, and operational knobs all live behind one programming model and one operational surface. The concrete wins follow:

  • Direct cost retirement – The push subscription, the maintenance-flag service, the function host, PlayFab itself, and the custom observability bridge all go away.
  • Cheat-proof logic by default – Game logic executes optimistically on the client and re-runs server-side, with no HTTP-validation workarounds in client code.
  • One mental model – One place to look when something breaks, one set of tools to operate and scale.

After the migration the team operates one programming model and one operational surface instead of half a dozen.

Dramatically less code and toil

The net change across client and server typically lands in the range of 15-20k lines deleted against 6-10k added. Most of the deletion is whole subsystems that exist only to cope with PlayFab and Azure quirks:

Workaround Reason it exists Fate
Per-write key-count chunking PlayFab UpdateUserData per-call key cap Deleted
Client request throttle PlayFab per-key rate quota Deleted
Split-key Title Data merge PlayFab 1MB-per-key limit Deleted
Retry-wrapped segment fetch PlayFab segment-API rate limits Deleted — segments are local
"Only write changed values" TODOs Reduce PlayFab round-trip cost Moot — full player state serialized on persist
FTUE-piggyback params on mutations Amortize PlayFab request cost Actions are cheap and split cleanly
USER_SESSION_DATA_READ_ONLY polling anti-cheat Detect cross-device session steal Old session kicked automatically
TITLE_DATA_HASH client disk-cache Avoid refetching Title Data per launch Design data is CDN-delivered and content-hashed
Reflection-dispatch function router Skip Function-per-endpoint boilerplate Type-safe action routing
Per-call PlayFab server-instance construction Multi-title config One deployment per environment

Around that table sits the structural plumbing — request wrapping, retry orchestration, the mail orchestrator, push and maintenance-flag SDK glue — none of it structurally interesting, all of it toil. A smaller surface is cheaper to test, maintain, and onboard new engineers onto. The full deletion catalog lives in What gets deleted outright.

Synchronous, optimistic client

On PlayFab every gameplay operation is an async network call. The client UI shows a waiter, awaits the round-trip, pattern-matches on a per-call error-code enum that grows to roughly a hundred values, then applies returned deltas and fires a local event. Every interaction stalls on HTTP latency.

On Metaplay the client executes the action locally first — optimistic UI, no waiter — while the SDK ships it to the server in the background. Animations, reward popups, and inventory updates fire immediately; the server catches up, and on the rare divergence the session resyncs. UI latency collapses from a network round-trip to a local function call.

The cleanup downstream of that change is substantial: async signatures no longer thread through the UI callstack, the wait-for-login loops disappear, the visible rate-limit UX goes away, and the per-call error-code enum stops being a UI concern.

Client and server stop drifting

On PlayFab, currency math, progression curves, gear stat rolls, and quest evaluation typically have two implementations — one on the client for UI previews and transient calculations, one in the serverless function host for the authoritative answer. Keeping the two in step is code-review discipline.

On Metaplay the gameplay logic is one project compiled into both client and server. There are no copies to update; the compiler is the check. For random rolls (gacha, gear drops), the server issues the action with the result baked into its payload and the client replays the same code with the same inputs. Drift becomes impossible by construction, and the runtime compares state checksums after every action and resyncs on any divergence.

A side benefit: the deterministic model enables replay-based reproduction from captured action streams. Not turnkey out of the box, but the programming model supports it.

Strongly typed and testable end-to-end

The PlayFab stack hands the engineer reflection-dispatched function calls returning loosely typed responses, JSON-shaped definition data, and opaque GUID lookups. None of that survives the migration. Metaplay replaces each surface with a typed one:

  • Game definitions – Strongly typed config libraries with compile-time-checked cross-references. Keys are human-readable string IDs, so the operations surface becomes navigable.
  • Player operations – Typed action classes with typed parameters. Reflection-dispatched routing becomes compiler-checked routing.
  • Storage – Tagged binary serialization with stable field tags. Adding a field is a single annotation.
  • Analyzers – Build-time rules catch determinism violations before they reach a runtime.
  • Tests – Actions are pure functions over the player state. The SDK ships fixtures for driving actions against synthetic players; seeded RNG makes loot-generating tests reproducible. A headless bot client drives end-to-end runs.

The function-host code in a PlayFab project rarely has a visible test harness because PlayFab round-trips are painful to mock; on Metaplay the typed surface and in-memory state make tests cheap to write.

Faster at runtime

PlayFab and a serverless function host tax every player action with HTTP round-trips, JSON design-data parsing, and per-call cold-start work. Metaplay keeps the player state and game definitions resident in the server-side actor, so the runtime cost of an action drops to a function call. The wins are concrete:

  • No per-mutation HTTP round-trip blocks the UI – Optimistic execution covers latency. See the previous subsection.
  • No fetch-mutate-write per RPC – Player state lives in memory in the actor and game definitions are loaded once. The PlayFab-side pattern of fanning out save-key and definition-map fetches per call disappears.
  • No cold-start reflection tax – The reflection-dispatch router does uncached type and method lookups per request, plus a design-data fan-out on the first call after a cold start. The actor model is warm and typed.
  • Persistent session connection – A long-lived push channel replaces a fresh HTTPS connection per action.
  • Efficient binary design-data format – Faster to parse than JSON, more compact on the wire, content-hashed local cache skips redownload on unchanged data.

The same player action that costs a network round-trip on PlayFab costs a function call after the migration.

First-class LiveOps and a cheat-proof economy

A typical PlayFab project has no LiveOps dashboard, no A/B testing, no experimentation framework, no push-campaign authoring, no customer-support tooling beyond raw PlayFab screens, and maintenance flags living in a separate vendor. Metaplay ships all of this:

  • Operations dashboard – Broadcasts, push campaigns, experiments, dynamic offers, player ops, maintenance mode, audit logs, and GDPR workflows on one surface.
  • Experiments – Server-side assignment with per-variant configuration overlays stacked on the base config.
  • Unified segments – Push, mail, experiments, and offers target the same predicate language, evaluated locally with no rate-limit concerns.
  • Signed admin API – Per-endpoint permission guards and named customer-support roles (game admin, game viewer, CS senior, CS agent) replace the absent admin/CS API of a typical PlayFab project.

The authority story improves in lockstep. Open cheat endpoints reachable by any client — a common artifact of PlayFab projects — close as a side effect of marking those actions as development-only. Banned-player enforcement kicks the session immediately instead of waiting for the next client poll. Every currency grant, drop, and unlock runs through a server-re-executed action. State tampering, cheat-endpoint abuse, and inflated grants all close at the same time.

Simpler, scalable data model

PlayFab forces the design data and the player save into shapes that the content team and the engineers work around daily. Metaplay replaces both with one model each. On the design-data side:

  • One archive – A single binary archive, CDN-delivered and content-hashed, downloaded once per config change. Replaces the typical fan-out of JSON Title Data keys split to fit the 1MB-per-key limit.
  • Designers author in spreadsheets – Builds trigger from the editor or the dashboard. No engineer in the loop for content edits.
  • First-class advanced features – Server-only field stripping, per-experiment variant configs, and CDN-delivered localization archives all come out of the box.

On the player-save side, one player model replaces the read-only and user-data key split that PlayFab forces:

  • One state class – Replaces dozens of read-only and user-data keys. The split was a PlayFab permissions trick; on Metaplay, client-writability is at the action level, not the field level.
  • One schema version – Field-level migration methods sit next to the changed field, replacing the two legacy version stamps and the separate migration runner that PlayFab projects accumulate.
  • Full-state persistence – Removes the "only write changed values" TODO class and the per-call key-count hazard.
  • Greenfield baseline – Schema v1, no history to carry.

Game analytics lands in the same shape: typed event types defined alongside the gameplay code, emitted from inside actions, exported to a data warehouse out of the box. The data model after the migration is one player state, one design-data archive, one event type hierarchy — not three split storage formats.

Built for AI-assisted development

A typed, in-repo, deterministic backend is the shape that modern AI coding assistants handle best. PlayFab and a serverless function host actively resist it — reflection-dispatched routing, JSON shape drift, and secrets and behavior split between vendor consoles all hide the contract from any tool reading the code. The Metaplay programming model removes that resistance and adds explicit tooling on top:

  • Reusable migration playbook – Many of the patterns a PlayFab project needs to port — auth bootstrap, save-model conversion, RPC-to-action shape, mail and segment retargeting — are documented from prior migrations. Only a few subsystems need bespoke handling.
  • First-party tooling aimed at AI agents – Documentation tuned for retrieval, MCP servers that let agents query the SDK and dashboards directly, and a Metaplay Developer agent in beta. Whatever assistant a studio already uses plugs into the same surfaces.
  • An architecture LLMs handle well – Typed surfaces end-to-end, mechanically repetitive ports, and build-time analyzers that turn determinism mistakes into errors give an assistant a clear feedback loop. Action-level unit fixtures, the headless bot client, and dry-run validation ground its output empirically.

The compounding effect is that first-pass quality from an AI assistant working in a Metaplay codebase is meaningfully higher than on the PlayFab and serverless equivalent.

A template the next game inherits

Once one title runs on Metaplay, every subsequent title at the studio inherits the programming model, save format, operations surface, and deployment pattern. LiveOps operators, customer-support agents, data engineers, and designers work against one set of tools across the portfolio — the platform decision is made once, not per game.

The savings show up per-feature too. Adding a new gameplay system on PlayFab means new save keys (annotated and chunked), new functions in the function host with reflection registration, new request DTOs and retry-loop subclasses, client-side façade wrappers, error codes, design-data maps split to fit the per-key limit, and potentially new orchestrations against blob storage.

On Metaplay the same feature is a small set of player-state fields, a handful of action classes, optionally a config library, optionally an event type. Many common systems are Metaplay-provided and adopt-not-build: leaderboards, mail, broadcasts, push, experiments, offers, IAP, analytics, maintenance. Guild-like structures sit on top of the multiplayer-entity model. Anything else extends through custom actors and actions. Time-to-first-feature drops from backend plumbing to business logic.

PlayFab migration timeline

How long the migration takes depends on how much of the source stack has to move, not on a one-size schedule. Two projects on PlayFab can differ by a factor of three in calendar time because one has a handful of gameplay subsystems and a vanilla integration layer while the other has dozens of helper classes, a custom multiplayer engine integration, and a bespoke mail pipeline. This section names the levers that set the duration, the sequencing that holds regardless of duration, and an illustrative shape from prior migrations to calibrate scoping conversations.

What determines migration duration

Duration is governed by a small set of levers. The scoping conversation at the start of an engagement walks each one and lands on a project-specific range:

  • Server-logic surface – The count of server-side endpoints to port drives the action-class inventory. A small PlayFab project might have a few dozen; a mature live game can have over a hundred. Most ports are mechanical once the pattern is in place.
  • Helper-class inventory – The shared logic that today lives in client and server copies — currency math, progression curves, requirement checks, drop rolls — has to land in one shared place. The more helpers in scope, the more determinism review the team does.
  • Gotcha set present in the source – A handful of PlayFab-side patterns add real work when they exist: a custom multiplayer engine integration, a custom IAP receipt flow, a custom mail orchestration on top of a function host, custom segment evaluation, or a custom analytics sink. A project carrying all of them takes meaningfully longer than one that uses the vanilla integrations.
  • Team size and engagement model – One customer engineer working with a Metaplay engineer is a different shape from a customer team running the port with Metaplay reviewing. Both work; they set different calendars.
  • AI-assisted leverage – Repetitive conversion work — RPC-to-action rewrites, save-key annotation, definition-data importers — compresses substantially with AI assistance. Projects that lean into it move faster on the mechanical phases.

The actual number of weeks falls out of these answers, not the other way around. The scoping conversation collects them; the timeline follows.

Sequencing constraints

The order of work is fixed even when the calendar is not. Four phases run in sequence, with parallel workstreams inside each:

  • Foundations – The Metaplay SDK lands in the project, the player-state class is scaffolded against the existing save keys, the design-data libraries are imported from the project's source of truth, and authentication and per-environment deployment come up. Everything downstream assumes these are in place.
  • Logic port – Shared helpers move into the shared-code project under determinism rules; server endpoints become typed action classes; the client retargets onto the new action API. Helper ports and action ports run in parallel once foundations are solid.
  • Integration – Mail, segments, push, IAP, analytics, and any custom multiplayer or matchmaking layer cut over onto Metaplay-side equivalents. Each integration is its own workstream and can run in parallel with the others once its endpoints exist.
  • Cutover – Staging soak, then production. The team validates against a headless bot run, runs experiments on segments, and switches live traffic. Rollback paths stay open until the soak ends.

Within a phase, work parallelizes. Across phases, the order holds — the logic port assumes foundations exist, integration assumes the logic port is far enough along to retarget against, and cutover assumes integration is feature-complete on staging.

Reference shape from past projects

The table below sketches three project shapes that turn up in PlayFab → Metaplay engagements. The duration column is illustrative across prior projects — not a commitment for any specific engagement, and not a default to apply without the scoping conversation above.

Project shape Subsystem footprint Indicative duration
Small Handful of gameplay subsystems beyond the core save/auth/login. Modest helper inventory. No custom multiplayer integration, no custom IAP flow, vanilla mail. A few dozen server endpoints to port. A few weeks
Medium Loadouts, quests, battle pass, mail, segments, and push all in scope. Dozens of helper classes shared between client and server. Vanilla multiplayer or none. Around a hundred server endpoints. A couple of months
Large Many gameplay subsystems, a custom multiplayer engine integration, a custom mail orchestration on top of a function host, custom segment evaluation, and a custom analytics sink. Over a hundred server endpoints. Mature live game with operational obligations during the migration. A quarter or more

The right input for any specific project is the lever count from the previous subsection, not the row that visually fits.

Decisions to surface early

A short list of calls needs to land in the first phase of the engagement. None are hard blockers — work continues on parallel tracks while each is resolved — but each one shapes scoping or product surface, so naming them early prevents a stall later.

Hosting model

Run on Metaplay Cloud (the managed offering) or on Private Cloud (self-hosted on AWS). Cloud is faster to stand up, removes operational toil, and fits most studios. Self-host fits when compliance, data residency, cost-at-volume, or studio-wide infrastructure standardization push that way.

Recommendation: start on Metaplay Cloud and migrate later if volume and operations call for it. Deeper trade-offs in the risk register and the hosting gotcha.

Analytics destination

Pick the event sink and whether the metrics endpoint stays separate. Shipped paths cover a data warehouse (long shelf life, ad-hoc query) and an APM sink (a few days of custom-sink work, real-time dashboards). Both is a valid answer when LiveOps and data science have different questions.

Recommendation: data warehouse for the core event sink unless the team already lives in an APM tool. Component-level detail in Analytics and telemetry.

Design-data source of truth

Pick where designers author. Shipped paths are spreadsheets (designer-driven, build-triggered) or a CSV export from an existing pipeline (engineer-mediated path preserved). Status-quo authoring layered on top of a custom exporter also works. The choice changes daily workflow for the content team, not the runtime.

Recommendation: defer to whatever the designers prefer; the engineering side is roughly equivalent either way.

Photon Quantum validation scope

For projects on Photon Quantum (the most common deterministic ECS choice in this stack) — or another custom multiplayer engine integration — pick how much match-result authority sits in the client. Trusting the client on results is cheapest but leaves the economy exposed. Routing match results through a server-side match-result entity validates the economy at the cost of more port work.

Recommendation: route the economically meaningful results — currencies, drops, progression unlocks — through the server; let cosmetic outcomes stay client-trusted. Detail in the Photon Quantum gotcha.

Push-notification cutover timing

Pick whether the existing push provider retires in the same window as the backend cutover or runs in parallel for a soak period. Cutover-together is cleaner; a parallel soak reduces risk for studios with heavy push-driven KPIs.

Recommendation: cutover together unless push drives a meaningful share of return traffic, in which case soak in parallel and switch when the new path is steady.

IAP cutover sequencing

Pick the order to cut over receipt validation across stores (App Store, Google Play, Steam, Amazon, Samsung, others). All can move at once; staggering reduces blast radius. The storefront wrapper inside the client does not need to change in lockstep.

Recommendation: start with the smallest live store as a canary, then move the largest once the canary is steady.

Part 2: The technical plan

Engineering detail behind the migration plan. This part outlines the key architectural differences between PlayFab and Metaplay and how to map concepts from one to the other — for technical staff who want to deep-dive into Metaplay and plan a migration in detail.

The architectural shift: PlayFab vs Metaplay

The single most important shape change for an engineer arriving at this migration: PlayFab projects run the client as a thin dispatcher to a stateless server; Metaplay shares the logic between client and server, runs the client predictively, and gives the server actor authority over the player model. The two flows below show the per-mutation path on each side.

Today (PlayFab + Azure Functions)

Client has no authority.
For every mutation:
  Client builds request DTO → queues it → HTTP → reflection-dispatch →
  serverless function fetches several save keys + several definition maps in parallel →
  mutates in memory → writes back changed keys (chunked to the per-call cap) →
  returns deltas → Client reapplies deltas locally.
Metaplay

Player state is a PlayerModel owned by a PlayerActor on the server.
Game logic lives in SharedCode — same code compiles into client and server.
For every mutation:
  Client executes PlayerAction locally against a mirrored PlayerModel (predictive).
  Action is sent to server.
  Server runs the same PlayerAction on the authoritative PlayerModel.
  Metaplay compares checksums and resyncs on divergence.
  Persistence is automatic.

See Creating and Using Actions for a hands-on introduction to the programming model.

Implications cascade

Once the per-mutation flow inverts, a long list of supporting code goes from load-bearing to obsolete. The big consequences:

  • Server endpoints collapse into actions – The set of serverless functions in scope becomes a set of PlayerAction classes in shared code. Same business logic, radically different wrapping: no async function-host plumbing, no PlayFab I/O, no JSON serialization at call time, no error-wrapper envelope, no explicit save-key writes. An action mutates PlayerModel in place and returns a typed action-result value.
  • Parallel fetch goes away – The pattern of fetching several save keys and several definition maps in parallel at the start of every call is obsolete. The PlayerModel is already resident in the actor; the game config is already loaded into a singleton.
  • Per-call write workarounds become moot – Per-call write-key caps, "only write changed values" TODOs, and the FTUE-piggyback pattern (mutations accepting an optional bundled-mutations parameter to save a round-trip) all disappear. Actions are cheap and the full PlayerModel persists on each save.
  • Client plumbing becomes Metaplay-provided – The client request queue, the retry loop, the session-polling anti-cheat, every per-endpoint request DTO — all of it is Metaplay-side now. The deletion catalog in What gets deleted outright tallies what comes out.

The result is that an entire layer of code stops being authored or maintained by the project team.

Target component inventory

The sections that follow lean on a small set of Metaplay-side primitives. This table is the reference for those terms; the rest of the guide uses these names without re-introducing them.

Primitive What it is
PlayerActor Server-side actor that owns the authoritative PlayerModel. One instance per player; lifecycle managed by the SDK.
PlayerModel Game-defined class extending PlayerModelBase. All save-state domains — wallet, inventory, progression, mailbox, segments — sit as fields on this class.
PlayerAction Typed mutation class that receives the model and mutates it in place; returns a MetaActionResult. The unit of work that replaces the server endpoint and the request DTO.
SharedCode project C# code compiled into both the Unity client and the server. Houses the player model, the actions, helper logic, and the analytics schema.
SharedGameConfig / ServerGameConfig Strongly typed design-data archives. SharedGameConfig ships to the client; ServerGameConfig carries the server-only extras.
BroadcastMessage / MailInbox The mail surface. Broadcasts target predicates; the mailbox lives on the PlayerModel. Replaces a custom mail orchestrator.
InAppProductInfoBase The IAP catalog as a config library, with platform-specific product IDs as fields.
PlayerCondition / PlayerSegmentInfoBase The predicate language behind segments. The same predicates power push targeting, mail targeting, experiment assignment, and dynamic offers.
AnalyticsEventBase / PlayerEventBase The analytics schema. Typed event classes defined in shared code, emitted from inside actions.
RuntimeOptions The server's operational knob surface. Subclasses of RuntimeOptionsBase expose runtime configuration the team edits without redeploying.

Example feature: levelling up a unit

This section walks one representative gameplay feature end-to-end on Metaplay. The feature — spending currency and XP to level up an owned unit in the player's roster — is generic enough to map onto most live game projects (heroes, vehicles, characters, ships, base modules) and concrete enough to show every primitive the rest of the guide references.

Headline: the roughly 200 lines of server endpoint, request DTO, and client retry glue that a typical PlayFab project needs for this feature collapse to around 45 lines of feature-specific code on Metaplay. The shared player model and design-data additions then amortize across every other unit-shaped feature in the game. Save-key fetch, JSON deserialize, definition fetch, write-back, and the retry loop disappear entirely — Metaplay handles them. Optimistic UI is free.

Concept mapping

The table maps the primitives a PlayFab project assembles for this feature against the primitives that replace them on Metaplay:

Today (PlayFab + serverless functions) On Metaplay Note
Server endpoint + client request DTO One PlayerLevelUpUnit : PlayerAction class + one-line client call The endpoint and the DTO collapse into the same class.
JSON save keys + manual deserialize Typed fields on PlayerModelUnits[UnitId], Wallet No JSON parsing at call time.
Definition-map fetch returning JSON-shaped config player.GameConfig.Units[UnitId] Resident in memory, typed.
Opaque numeric unit IDs UnitId : StringId<UnitId> Human-readable, navigable in operations.
Per-call error-code enum returned in a response envelope Typed MetaActionResult values in one action-result registry Failure modes are values, not strings.
Chunked save-key write-back at end of call Metaplay persists the mutated PlayerModel automatically No explicit write step.
Client awaits HTTP, applies returned deltas to local state Action runs locally first; server replays authoritatively and resyncs on divergence Optimistic UI by default.
Dispatch chain — request queue, retry loop, function-host call, reflection routing Gone — Metaplay handles the wire Removed wholesale.
FTUE-bundled-mutations parameter on the same endpoint Separate PlayerSetFtueCheckpoint action Actions split cleanly because they are cheap.
Validation requires a separate endpoint DryExecuteAction on the same class Same code path, no commit.

The mapping is general; the same structure applies to a shop purchase, a quest claim, or any other state-mutating feature on the player model.

What the Metaplay implementation looks like

In shared code the feature is three small additions: a slice of the player model, a design-data library entry, and one action class. The client invokes the action with a single line. The samples below are close to what the ported logic looks like in practice — names and shapes will land near these.

The player model is the single source of truth for everything per-player that mutates over time. The server owns it authoritatively; the client holds a mirror that actions mutate. Typed fields replace the JSON-shaped save keys a PlayFab project carries. See Saving Player Data on the Server for the conceptual introduction:

// PlayerModel: server-authoritative player state. Persisted by Metaplay, mirrored to the client.
public class PlayerModel : PlayerModelBase<PlayerModel, /* ... */>
{
    [MetaMember(110)] public MetaDictionary<UnitId, UnitModel> Units  { get; private set; }
    [MetaMember(111)] public PlayerWallet                      Wallet { get; private set; }
}

// UnitModel: per-unit player state — current level and XP progress toward the next level.
[MetaSerializable]
public class UnitModel
{
    [MetaMember(1)] public UnitId UnitId    { get; private set; }
    [MetaMember(2)] public int    Level     { get; set; }
    [MetaMember(3)] public int    CurrentXp { get; set; }
}

The game config is the read-only design data — unit stats, currency costs, level caps, drop rates — authored by designers and compiled into a binary archive shipped over a CDN. It replaces what a PlayFab project keeps in title-data keys and definition maps. Client and server load the same archive, so balance lookups are local and identical on both sides:

// UnitId: stable, readable key for a unit (e.g. "starter_unit") — replaces the long opaque IDs.
[MetaSerializable]
public class UnitId : StringId<UnitId> { }

// UnitInfo: per-unit design data — XP curve, credit-cost curve, level cap.
[MetaSerializable]
public class UnitInfo : IGameConfigData<UnitId>
{
    [MetaMember(1)] public UnitId Id             { get; private set; }
    [MetaMember(2)] public int    MaxLevel       { get; private set; }
    [MetaMember(3)] public int[]  XpPerLevel     { get; private set; }
    [MetaMember(4)] public int[]  CreditPerLevel { get; private set; }

    public UnitId ConfigKey => Id;
    public int XpToReach(int from, int to)      => XpPerLevel.Skip(from).Take(to - from).Sum();
    public int CreditsToReach(int from, int to) => CreditPerLevel.Skip(from).Take(to - from).Sum();
}

// SharedGameConfig: top-level container for all design libraries — one entry per CSV/Sheet.
public class SharedGameConfig : SharedGameConfigBase
{
    [GameConfigEntry("Units")]
    public GameConfigLibrary<UnitId, UnitInfo> Units { get; private set; }
}

The action is one atomic, deterministic mutation of the player model — level up a unit, claim a quest reward, buy from the shop. Each action validates inputs against config and current state, then mutates. Metaplay runs the same action on both client (for instant UI) and server (authoritative) and reconciles via checksum. Actions replace the per-feature server endpoints a PlayFab project carries:

// ActionResult: failure modes for LevelUpUnit — the action below returns one of these on rejection.
public static class ActionResult
{
    public static readonly MetaActionResult MaxLevelReached = new(nameof(MaxLevelReached));
    public static readonly MetaActionResult LevelCapped     = new(nameof(LevelCapped));
    public static readonly MetaActionResult NotEnoughFunds  = new(nameof(NotEnoughFunds));
}

// PlayerLevelUpUnit: invoked when the player taps the level-up button —
// spends XP + Credits to raise a unit to TargetLevel.
[ModelAction(ActionCodes.PlayerLevelUpUnit)]
public class PlayerLevelUpUnit : PlayerAction
{
    public UnitId UnitId      { get; private set; }
    public int    TargetLevel { get; private set; }

    public PlayerLevelUpUnit(UnitId unitId, int targetLevel)
    {
        UnitId      = unitId;
        TargetLevel = targetLevel;
    }

    // Runs on both sides — client predicts for instant UI, server is authoritative.
    // Metaplay reconciles the two via checksum.
    public override MetaActionResult Execute(PlayerModel player, bool commit)
    {
        UnitInfo  info = player.GameConfig.Units[UnitId];
        UnitModel unit = player.Units[UnitId];

        if (TargetLevel > info.MaxLevel)
            return ActionResult.MaxLevelReached;
        if (TargetLevel > player.GetUnitLevelCap())
            return ActionResult.LevelCapped;

        int xpCost     = info.XpToReach(unit.Level, TargetLevel) - unit.CurrentXp;
        int creditCost = info.CreditsToReach(unit.Level, TargetLevel);
        if (xpCost > player.Wallet.Xp || creditCost > player.Wallet.Credits)
            return ActionResult.NotEnoughFunds;

        if (commit)
        {
            unit.Level             = TargetLevel;
            unit.CurrentXp         = 0;
            player.Wallet.Xp      -= xpCost;
            player.Wallet.Credits -= creditCost;
        }
        return MetaActionResult.Success;
    }
}

The client invocation site is what UI screens actually call. It replaces the async request-DTO chain a PlayFab project threads through the UI with one synchronous line per action; Metaplay handles the round-trip and the optimistic-UI bookkeeping:

// Thin facade UI screens call. Replaces the async LevelUpUnitAsync request DTO.
public void LevelUpUnit(UnitId unitId, int targetLevel)
{
    // Runs the action locally for instant UI feedback, then sends it to the
    // server for authoritative replay.
    MetaplayClient.PlayerContext.ExecuteAction(
        new PlayerLevelUpUnit(unitId, targetLevel));
}

// Same action, validated without mutating — used to grey out the level-up
// button when the player can't afford it.
MetaActionResult result = MetaplayClient.PlayerContext.DryExecuteAction(
    new PlayerLevelUpUnit(unitId, targetLevel));
bool canAfford = result.IsSuccess;

The shape generalises. Adding the next unit-like feature reuses the same three pieces — a model slice, a config library entry, one action class — and the client invocation drops to a one-liner.

SharedCode extraction and determinism

The hardest mental shift for engineers arriving from PlayFab is not the API surface — it is the new constraint that a chunk of game logic now compiles into the client and has to run identically on both sides. On PlayFab the server was a black box; nothing about the language or the runtime forced the team to think about client/server agreement. On Metaplay, the client runs the same code the server runs, predictively, and Metaplay compares checksums on every action. Drift terminates the session and resyncs. This section lays out the constraint, the toolkit that satisfies it, and the escape hatch for the cases where determinism is the wrong answer.

What SharedCode is

SharedCode is one C# project whose source lives inside the Unity Assets/ tree. Unity compiles it as part of the client build; a companion server-side .csproj file-includes the same sources, so client and server run identical bytecode for the shared logic. The project houses the PlayerModel class, every PlayerAction, the strongly typed GameConfig definitions, the analytics event schema, and any helper code reachable from an action. Roslyn analyzers run on both sides at build time and reject the most common determinism violations. See Game Logic Execution Model for the full reference on how shared code runs and reconciles between client and server.

What must be deterministic

The boundary is precise: anything reachable from a PlayerAction must be deterministic. Fields not marked [NoChecksum] must produce byte-identical values on client and server given the same inputs. Metaplay compares checksums continuously; on mismatch the session terminates and reconnects with a fresh authoritative state.

That boundary covers more than it first looks like:

  • The player model itself – Every [MetaMember] field on PlayerModel and the types it transitively references. The serialised bytes are what gets checksummed.
  • Action bodies – The code inside every Execute method on a PlayerAction. Both sides run it, both sides must reach the same state.
  • Helper code called from actions – Any utility — wallet math, progression rules, requirement checks, reward grants — that an action's body calls into is reachable and counts.
  • GameConfig reads – Strongly typed and deterministic by construction. Reading gameConfig.Units[unitId].MaxLevel is allowed and cheap.

What sits outside the boundary is anything server-only — server event handlers, dashboard player ops, scheduled jobs, server actors that are not PlayerActor. Those can use ordinary DateTime, System.Random, Dictionary<K,V>, and async I/O freely. The constraint is the action path, not the entire server.

Determinism toolkit

The Metaplay SDK ships a small set of replacement primitives that make satisfying the constraint mechanical. The pattern is substitution: where shared code wants to do X, use the deterministic replacement Y instead.

Need Stdlib answer (banned) Deterministic replacement
Floating-point math float, double F32, F64 fixed-point types
Randomness System.Random, Guid.NewGuid() RandomPCG field on the model
Wall clock DateTime.UtcNow, MetaTime.Now player.CurrentTime tick-derived clock
Map collection on a [MetaMember] field Dictionary<K,V> MetaDictionary<K,V>
Set collection on a [MetaMember] field HashSet<T> OrderedSet<T>
Timestamp on a [MetaMember] field DateTime MetaTime or DateTimeOffset
Identifier inside design data Guid, opaque integer StringId<T>

A handful of these warrant their own note:

  • Fixed-point mathF64 / F32 produce bit-identical results across ARM and x86, which double and float do not. Stat curves, drop weights, currency formulas, time-decay functions all want the fixed-point types if they live in shared code. See Fixed-Point Math.
  • RandomnessRandomPCG lives as a field on PlayerModel. The seed persists with the model, so subsequent draws are reproducible from the same starting state — and bot replays, balance sweeps, and bug-repro from a player save all work for free. new Random() and Guid.NewGuid() must not appear inside actions. See Implementing Cheat-Proof Randomization.
  • Time – Inside actions, player.CurrentTime is the tick-derived deterministic clock that both sides agree on. MetaTime.Now is the wall clock and is forbidden in shared code — it would diverge by definition. See Time and Ticks.
  • CollectionsMetaDictionary<K,V> and OrderedSet<T> replace their stdlib equivalents on [MetaMember] fields, enforced by a Roslyn analyzer. The stdlib hash containers do not guarantee iteration order across .NET versions; the deterministic variants do. See Deterministic Collections.

The Roslyn analyzers catch the structural violations — disallowed collections on annotated fields, async in an action body, non-MetaSerializable types in serialised positions. The call-site bans on DateTime.UtcNow, Guid.NewGuid(), and new Random() inside shared code are code-review discipline, not analyzer-enforced; teams typically add their own analyzer pass for those once the migration settles.

Server-rolled results

Some operations cannot be deterministic by construction: loot rolls that must not be client-spoofable, lookups that depend on server-only state, integrations with external services. On PlayFab these were handled by keeping the entire endpoint server-side and returning an opaque payload — the client knew nothing about the roll. Metaplay needs the same affordance, since the predictive client cannot be allowed to fabricate a favourable result.

The mechanism is the server-issued action: an action that executes first on the server, then replays on the client with the non-deterministic result baked into the payload. The client never rolls; it receives the rolled result and applies it. Fields that the server writes from inside such an action are marked [NoChecksum] so the checksum comparison still passes. See Custom Server Logic.

Typical uses:

  • Loot box and gacha rolls – The pull weights live in ServerGameConfig, the roll uses a server-side RandomPCG, and the action payload carries the chosen item IDs. The client sees a spinner, the server rolls, the result replicates. The current product UX already waits on a round-trip for this — there is no perceived latency loss.
  • Random gear or stat generation – Same shape: stat ranges live in ServerGameConfig, the roller is a server-only helper, the resulting gear instance lands in the action payload. No need to audit the math for F64 correctness, because the rolling code never runs on the client.
  • External lookups baked into actions – A receipt-validation result, a moderation API response, a third-party balance check — the server resolves it, the action carries the resolved value, the client replays without retrying the lookup.

The split is therefore: deterministic-by-nature helpers — wallet math, progression curves, requirement evaluation, integer-only quest progress — move into SharedCode and run on both sides; randomness-driven helpers and server-only lookups stay server-side and the action carries their output. The reader should not feel that the determinism constraint is restrictive — most game logic is deterministic-by-nature, and the server-rolled escape hatch covers the rest cleanly.

Port checklist for each helper class

Every helper class in the source project gets walked through the same checklist. The shape of the audit:

Requirement What to look for Action
Deterministic across client/server new Random(), Guid.NewGuid(), server-only state reads Use player.Random, or keep the helper server-only
Deterministic across ARM/x86 float float / double math in stats, weights, prices, curves F64 if the helper moves to SharedCode; leave alone if it stays server-only
Synchronous (no async) Helpers signed as async Task<…> for PlayFab or telemetry calls Remove async; pass pre-resolved GameConfig and player state in as parameters
No external I/O PlayFab API calls, blob reads, HTTP calls, telemetry emits inline Extract the I/O to the caller; the pure logic stays
[MetaSerializable] types Newtonsoft-decorated DTOs in helper signatures Re-annotate the DTOs with [MetaSerializable] + [MetaMember(N)]
No wall-clock time DateTime.UtcNow, custom DateTimeUtil helpers player.CurrentTime
Deterministic iteration foreach over Dictionary<,> or HashSet<> on a serialised field Switch the field type to MetaDictionary / OrderedSet

This is the right checklist to hand each engineer working through the helper inventory in parallel. Most helpers come out clean after the first three rows; the rest are quick mechanical fixes.

PlayerModel lifecycle hooks

A chunk of code that on PlayFab lived in a session-warmup function — the one that ran at the top of every login to repair stale state, accumulate offline time, run feature unlocks — moves onto lifecycle hooks on PlayerModelBase. The hooks fire server-side; the client receives already-updated state.

Hook When it fires What lands here
GameInitializeNewPlayerModel Brand-new account, before experiment assignment Default inventory grants, starting wallet, default loadout, baseline progression
GameOnInitialLogin First session only, after experiment assignment One-time new-account work that needs to know the assigned experiment variants
GameOnSessionStarted Every session start Session-start fixups — the bulk of what used to live in the post-login task runner
GameOnRestoredFromPersistedState Every wake from persistence (online or offline) Offline-time accumulation, config-driven repairs, daily-reset logic

The split between GameOnSessionStarted and GameOnRestoredFromPersistedState is the one that catches teams out: online ticks fire only the former; an offline-then-wake fires the latter first, then the former. Time-accumulation logic (passive income, energy regen, daily login windows) belongs in GameOnRestoredFromPersistedState so it runs on both paths. See Initializing Data on the Player Model.

The post-login task runner that on PlayFab dispatched a series of small repair functions disappears: each former task becomes a direct call from the appropriate lifecycle hook, with no orchestration layer in between.

Unity client-side refactor

The client side is the part of the migration most teams expect to be a rewrite and that in practice is a retarget. The reason is a quiet architectural choice that most mature PlayFab Unity projects converge on: the UI does not call PlayFab directly and does not await HTTP responses inline. Instead, a single client-side backend façade fires C# events ("shop item bought", "unit unlocked", "mission completed", "mail claimed") that the UI subscribes to. Buttons enqueue a request; the façade fires an event when the server confirms; the UI reacts to the event.

That shape happens to be exactly the shape Metaplay's client uses — UI subscribes to changes on the mirrored PlayerModel. The migration strategy follows: keep the events, replace what fires them. Most of the UI layer does not need to know that the backend changed.

What the dispatch sites turn into

Every façade method that today wraps an HTTP-dispatched server endpoint collapses to an ExecuteAction call against the player context. The signature shrinks because there is no asynchronous result to await — the predictive Execute mutates the local model synchronously and the UI reads the mutation back through the same event the façade was firing before.

// Today (client-side façade method)
public async Task<AFResponse<BuyShopItemResult>> BuyShopItemAsync(
    string shopItemId)
{
    var response = await _requestQueue.Enqueue(
        new BuyShopItemRequest { ShopItemId = shopItemId });

    if (response.IsSuccess)
        ShopItemBought?.Invoke(shopItemId);

    return response;
}
// Metaplay
public void BuyShopItem(InAppProductId shopItemId)
{
    MetaplayClient.PlayerContext.ExecuteAction(
        new PlayerBuyShopItem(shopItemId));

    ShopItemBought?.Invoke(shopItemId);
}

The asynchronous wrapper, the error envelope, and the request DTO all disappear. Server-side refusal surfaces through the standard action-result handling instead of an inline IsSuccess check; UI code that today inspects the response payload reads the same fields off the mirrored PlayerModel after Execute returns.

What gets deleted at the client boundary

A handful of client-side subsystems that exist only because of PlayFab's transport go away entirely. The deletion catalog in What gets deleted outright tallies the full list with reasons; for the client refactor the load-bearing items are:

  • The client request queue – The per-second throttle, the retry-on-failure wrapper, the offline-queue behaviour — all owned by Metaplay's transport now. The custom queue and its calling conventions delete outright.
  • The retry/network base class – Every *Request.cs subclass extending a project-wide retry/network loop disappears with the queue. Hundreds of lines, none replaced.
  • Per-endpoint request DTOs – Each former endpoint had a matching client-side request DTO that serialised arguments and unwrapped the response. Replaced by the typed PlayerAction class on the SharedCode side; nothing needs to live on the client.
  • Login-time fan-out reads – The startup sequence that fetched the player save key-by-key, then the design data key-by-key, then ran a local merge — gone. Metaplay's session-start replaces the mirrored PlayerModel and resolves the game-config archive in one transport phase.
  • PlayFab service wrappers – The auth bootstrap, the authenticator, the service-wrapper class around the PlayFab SDK — thinned to a small Metaplay bootstrap that performs login and hands the client the session context.
  • The client-side definition cache – The cache layer that today wraps every design-data lookup retargets to the Metaplay game-config runtime. The public accessor API the UI calls (Definitions.Units[id]-style) can usually be preserved with a one-line redirect into the typed GameConfig library, which keeps every call site intact.

The net is that an entire transport, retry, queueing, and cache layer comes out of the client without a replacement on the project side — the SDK is the replacement.

Client-side PlayerModel mirror

The remaining piece is how the client keeps its mirrored PlayerModel in sync with the authoritative one on the server. Three mechanisms cover every case:

  • Predictive Execute – When the client issues a PlayerAction, the SDK runs the action's Execute against the local mirror immediately. The UI sees the mutation without waiting for the server round-trip. The server runs the same action shortly after; Metaplay compares checksums and resyncs on divergence.
  • Server-pushed actions – Mutations the server initiates — broadcast mail delivery, an offer becoming available, a scheduled reward — arrive as actions on the model journal and apply to the mirror the same way client-initiated actions do.
  • Full state sync on session start – At every session start the mirror is replaced wholesale with the server-authoritative PlayerModel. Stale local state never persists across a reconnect.

UI code that today reads from the client-side definition cache or the local player save retargets to the mirror — playerContext.Model in place of whatever local object the project hands around. The reads do not change in shape; only the source does.

Predictive workarounds that retire

A side observation worth calling out: most mature PlayFab clients have built one or more bespoke "instant-feel" patches over the round-trip latency — provisional currency deductions that get reconciled when the response lands, optimistic inventory adds with rollback on failure, animation timing that hides the wait. These workarounds delete. The predictive Execute covers the same intent correctly and with rollback semantics that the SDK owns, not the project. The UI keeps its instant feel; the team stops maintaining the patches.

Estimated impact

A typical mature PlayFab Unity project sheds several thousand lines of client code on the net — illustrative range single-digit thousands net negative, dominated by the transport, queueing, DTO, and migration-runner deletions and offset by a small amount of glue code for the SDK bootstrap. The exact number depends on how much of the façade layer the project decides to keep verbatim versus simplify in passing.

What gets deleted outright

A large fraction of the work in a PlayFab project exists only because of PlayFab and Azure quirks. The catalog below names the load-bearing items that come out of the codebase without a project-side replacement on Metaplay. Earlier sections describe what each is and what replaces it; this section is the one-sided deletion view.

Thing Reason gone
Player-save bootstrap (create / update / delete flow) PlayerActor lifecycle owns this
Per-call key-chunking helper on the save path No per-call write cap on Metaplay
Custom PlayFab HTTP retry plugin PlayFab-specific transport
Title-Data split-key merge logic GameConfig is one archive
In-memory Title-Data cache GameConfig is resident in memory
Title-Data on-disk cache Content-hashed GameConfig archive on CDN
Title-Data hash key + redownload plumbing Archive hash handled by the SDK
Client request queue + per-window throttle Metaplay transport queues actions
Client retry / network base loop Metaplay handles resilience
Per-endpoint client request DTOs Typed PlayerAction classes replace
Login-time fan-out save reads Session-start replaces the mirrored PlayerModel
Save migration runner + dual version stamps [MigrateFromVersion] methods on the model
Custom mail scheduled task + orchestrators + activities BroadcastMessage and built-in mail
Custom mailbox classes + per-player blob containers MailInbox on PlayerModel
PlayFab inventory redemption shim + tracker key Native IAP grants on the model
Studio IAP storefront wrapper SDK's IAPManager owns Unity Purchasing
Server-time fetch + client-side time-skew override MetaTime is the source
Session-polling validator + read-only session key New connection kicks old at transport level
AFResponse<T> envelope + project-wide error enum MetaActionResult
Reflection-dispatch function router Typed action routing in shared code
UGS Remote Config maintenance / config fetcher MaintenanceMode + RuntimeOptions
CloudScript shim + entity-script helpers Not used; dead code
Segment evaluation helper + Polly retry wrapper Local segment evaluation against the player model
Push-notification service SDK + Android config Metaplay push notifications
Analytics-bridging app (Kusto-to-APM) Prometheus + native analytics sinks
Ad-hoc cheat endpoints reachable in production [DevelopmentOnlyAction] plus role-gated dashboard player-ops

Across client and server combined, a mature PlayFab project typically sheds between fifteen and twenty thousand lines on the net — an order-of-magnitude figure based on the items above, not a per-file tally. The deletions concentrate on the transport, persistence, and orchestration layers; the game-logic code that remains is the part worth keeping.

Common PlayFab migration gotchas

A small set of patterns recur across PlayFab → Metaplay migrations that catch teams by surprise even when the rest of the port is proceeding cleanly. Not every project hits every one; keep the items that apply to your codebase and drop the rest. Each entry below names the typical shape, what to do during the migration, and the destination shape on Metaplay.

Photon Quantum integration

Many PlayFab projects pair the backend with Photon Quantum for deterministic real-time multiplayer. The integration surface that teams typically build:

  • A Quantum custom plugin – Server-side plugin code that hosts the simulation, validates match-result submissions, and grants rewards on completion.
  • A PlayFab-backed auth provider – Photon authenticates clients by handing the session ticket to PlayFab and trusting the verification result.
  • Match-result reconciliation – Each client posts its view of the match result; the plugin cross-checks for agreement before granting rewards.

On Metaplay, the building blocks are first-class but the integration code is project-side. The relevant primitives:

  • Multiplayer Entities – General-purpose ephemeral or persisted server entities. A match-result entity can collect submissions from PlayerActors, validate cross-player agreement, grant rewards, and terminate (see Multiplayer Entities).
  • Public Web Endpoints – HTTPS endpoints a Photon server plugin can call inbound (see Public Web Endpoints).
  • Server-only actions and ServerGameConfig – Reward tables and validation thresholds stay server-side and are unreachable from the client.
  • Custom entity kinds + EntityAsk – Arbitrary server-side coordination patterns for things that fall outside the player or match entity (see Custom Entities and Entity-to-Entity Communication).

The Metaplay ↔ Photon auth bridge and the cross-player match-result validation flow are solved patterns on the Metaplay side; the migration brings them into the project's codebase. Synchronous matchmaker is not yet shipped, but Photon Cloud handles matchmaking in most of these projects already.

ID retyping for Metaplay-friendly types

PlayFab projects commonly carry a custom 64-bit-or-similar identifier type for in-game entities — units, items, missions, currencies — referenced throughout client and server code. The migration is the right moment to retype these to Metaplay's StringId<T> keys and MetaRef<T> cross-references, for two reasons:

  • Operations becomes navigable – Readable keys like starter_pack_v2 or daily_quest_login show up in dashboard player ops, audit logs, and analytics in place of opaque integers. The LiveOps team stops asking engineering "which item is 8472930482390?".
  • Greenfield has no migration cost – With no live player data to preserve, the retype is a scripted refactor across call sites rather than a data-migration project. Doing it later, on a live game, costs an order of magnitude more.

The scope is usually hundreds of call sites and roughly a week of focused work for a mature codebase. The carrying-over alternative — keeping the legacy ID type with a converter to Metaplay's types — works mechanically and may suit projects with a hard cutover window, but the operations surface stays opaque.

FTUE piggybacked onto session warmup

A typical PlayFab pattern: gameplay mutations accept an optional "FTUE operations" parameter so the FTUE state can ride along on the same request as the gameplay action. Built to amortise the PlayFab round-trip cost — every saved trip during onboarding matters at scale. The shape that accumulates:

  • Mutations carry a bundled-FTUE parameter – Every endpoint that fires during onboarding takes an optional FTUE-state delta, applies it alongside the gameplay mutation, and returns the merged result.
  • FTUE state lives in the same key spaces as gameplay state – To make the bundle possible, FTUE flags share read-only and user-data keys with the regular save.
  • Server-side validation is loose – Because the FTUE delta arrives as a parameter rather than a typed endpoint, the server cannot reject malformed FTUE transitions without bespoke validation.

On Metaplay, actions are cheap and there is no round-trip to amortise; the bundling is pure overhead. The migration splits each piggybacked FTUE delta into its own typed PlayerActionPlayerSetFtueCheckpoint, PlayerAdvanceFtueStep, etc. The gameplay actions go back to doing one thing each, and the FTUE transitions become inspectable and validatable in the usual way.

The cutover risk is forgetting an FTUE bundle: a player mid-onboarding during the cutover whose last gameplay action carried an FTUE delta that did not land server-side. Treat the onboarding cohort as a soak-test population — visible to QA, watched during the first production days.

Unbounded growth in player data blobs

PlayFab projects commonly accumulate a single player-data key that grows monotonically — a roll-history log, a mail-history log, an event-completion record, a battle-pass progression archive. The key fits comfortably for the first six months and becomes a performance hazard at the long-tail. The shape:

  • Append-only by intent – The list is the audit trail; nothing trims it because nothing was designed to.
  • Read on every relevant call – Even unrelated endpoints fetch the key as part of the save fan-out, paying the deserialisation cost on every request.
  • Hard to fix in production – On PlayFab there is no clean place to run a one-time pruner; the trim has to happen client-side or in a bespoke maintenance job.

Greenfield is the moment to redesign. Two patterns work on Metaplay:

  • Cap-in-place plus analytics – Hold the last N entries on PlayerModel; emit an AnalyticsEvent per entry so the long-term history lives in the analytics warehouse rather than on the player.
  • Rotate into a bounded window – Per-week or per-event buckets on the model, with buckets older than a threshold dropped on GameOnRestoredFromPersistedState.

Either is a small action's worth of work and saves the project from porting a known performance bug into the new platform.

Multi-environment deployment

A typical PlayFab setup splits dev, staging, and production across three PlayFab titles, three Function-app slots, three push-provider apps, and possibly three remote-config namespaces. Per-environment deployment is whatever script orchestrates that mosaic. On Metaplay, each environment is one independent deployment — its own database, blob storage, CDN, and dashboard — and three is the usual shape (see Cloud Deployments).

Two decisions surface that did not exist on PlayFab:

  • Environment promotion strategy – How a newly built game-config archive moves from dev → staging → production. Manual upload through the dashboard, scripted promotion via CI, or a hybrid. Who has publish rights at each step.
  • Per-environment RuntimeOptions – YAML files live per environment. The team decides whether they sit in the repo alongside game code or in a separate secure store, and how they reach the running server.

Neither needs to be settled up front. The workflow can be picked up at any point during the migration, and the answer is cheap to revisit once the team has a feel for the daily rhythm.

Hosting: managed or self-host

Two hosting options suit different points on the project lifecycle:

  • Metaplay Cloud (managed) – Simpler and cheaper to get started. Metaplay runs the infrastructure while the team focuses on game code. The recommended starting point, and most projects stay on it through launch (see Cloud Deployments).
  • Private Cloud (self-host) – Full control and ownership at scale. Suited where compliance, data residency, cost at high volume, or platform standardisation across studios pushes that direction. Provisioned via Metaplay's Terraform + Helm modules; AWS-only — GCP and Azure are not supported for self-host (see Private Cloud).

Starting managed and moving to self-host later is supported; the runtime is the same either way. The default recommendation is to start managed and revisit only when a concrete constraint forces the decision.

Photon ↔ PlayFab auth handshake

Photon Quantum projects commonly use PlayFab as the custom auth provider — the Photon plugin holds a webhook URL that points at PlayFab, and the client passes its session ticket through Photon's OnCustomAuthentication path. The PlayFab cutover therefore breaks the Photon auth path as a side effect.

The destination shape: Metaplay mints a session claim, the client passes it through Photon's custom-auth path, and the Photon server plugin verifies the claim against a Metaplay public endpoint. This is a solved pattern on the Metaplay side, landing in an upcoming SDK release and fast-trackable into a project if the timeline calls for it. Plan the auth-handshake swap to land in the same window as the gameplay cutover; running the two halves out of phase is the shape that breaks login during the transition.

Production clock manipulation

A typical PlayFab project carries a small testing affordance for manipulating server time — a server-time-override title-data key plus a client-side fetch helper, gated behind a debug flag. QA and LiveOps lean on it to validate time-of-day events, daily resets, and battle-pass progression without waiting on the wall clock.

Out of the box, Metaplay's Time Skip covers the same use case on dev and local environments — a forward-only MetaTime offset that makes time-based features testable in seconds rather than days. Time Skip is intentionally disabled in staging and production.

If production-side time controls are a hard requirement — rare, but LiveOps teams that drove operations off the PlayFab override sometimes ask — they are achievable with modest SDK additions. Surface the requirement during the Phase 0 conversation so the gap is visible before testing assumes it is there.

Testing strategy on Metaplay

The platform change reshapes what is cheap to test. On PlayFab, unit-testing a server endpoint means mocking the SDK's player-data and title-data calls accurately enough to drive the handler — a viable but high-friction exercise that most teams skip in practice, running their endpoint tests against a live PlayFab title in a dev environment instead. The result is a test pyramid that is heavy at the top (live integration) and thin in the middle (unit). Metaplay flips this: actions are pure mutations of an in-memory model, so the cheap unit-test layer is finally cheap, and the integration layer is a real Metaplay server driven by a programmable client. Three layers cover the test pyramid the project should target.

Unit testing PlayerActions

The SDK ships test fixtures that drive PlayerAction.Execute against a synthetic PlayerModel. A typical test is short — construct the model in the precondition state, execute the action, assert on the resulting state and the returned MetaActionResult:

[Fact]
public void BuyShopItem_DebitsWalletAndGrantsItem()
{
    var player = TestPlayer.NewFreshModel();
    player.Wallet.Soft = 500;

    var result = player.ExecuteServerAction(
        new PlayerBuyShopItem(ShopItemId.FromString("starter_pack_v2")));

    Assert.Equal(MetaActionResult.Success, result);
    Assert.Equal(0, player.Wallet.Soft);
    Assert.True(player.Inventory.Contains(ItemId.FromString("hero_axe")));
}

Two coverage targets are worth holding the team to:

  • Every action covers success plus each documented MetaActionResult – The action's contract is the set of result values it can return. Each is one branch in the implementation and one test in the suite. Skipping the refusal branches is where unit tests rot in practice.
  • Randomness-driven actions use a seeded RandomPCG – Reproducible bug repro from a player save depends on the seed being part of the input, not implicit state. Tests that drive a seeded action are also tests of the determinism guarantee.

The unit suite runs in seconds and parallelises freely. Teams that arrive at Metaplay from a PlayFab project where this layer was thin typically end up with several thousand action unit tests by the end of the migration.

Integration testing

The integration layer is a real Metaplay server, deployed to a staging environment or spun up in-process by the test harness, and driven through the SDK's client or through BotClient. The shape of what it covers:

  • Persistence round-trips – An action mutates the model; a forced wake-from-persistence replays the persisted state and the assertions hold.
  • Game-config delivery to new sessions – A freshly built config archive serves to a session started after the build; existing sessions keep the archive they were loaded with.
  • Broadcast pickup at session start – Players eligible during the broadcast window receive the mail at their next session start.
  • IAP sandbox flows – Google Play and Apple App Store sandbox credentials drive a real platform-validated receipt through the server's IAP path.
  • Experiment assignment – Players get assigned to the configured variant set at first login and the assignment persists across sessions.
  • Session lifecycle hooks – The four PlayerModelBase hooks fire in the right order on first login, on each session start, and on offline wake.

Property-based tests pair well with the deterministic helpers — wallet math, progression curves, requirement evaluation. The fixed-point arithmetic and seeded RNG mean a property-based runner can generate thousands of inputs and the failures it surfaces are reproducible from the seed.

Testing via BotClient

BotClient is a programmable headless client implementing game-specific AI that drives a real Metaplay server over the wire. It replaces the hand-rolled load-test scripts that PlayFab projects accumulate to hit endpoints with parameterised payloads. The three uses worth budgeting for:

  • Functional smoke test – Bots exercise the gameplay loop end-to-end on every deploy — login, core loop, rewards, shop, IAP sandbox. The smoke suite is the first signal that a regression landed.
  • Economy simulation – Long-running bots play the gacha, mission, and progression loops continuously, surfacing balance issues and determinism mismatches that point fixtures miss. Multi-day runs against staging are common.
  • Load-shape reference – The bot configuration is also the load-testing harness; scaling the bot population produces a load profile that resembles real play.

During the migration, BotClient does double duty: it is the mechanism that keeps integration coverage climbing as each PlayerAction lands, without the team having to hand-write a matching scripted scenario for every one. Allocate explicit time for the game-specific bot AI in the porting phase — it is small, but it is real engineering and it pays off across the rest of operations.

LiveOps and customer support surface

The migration's largest visible change for non-engineering stakeholders is that LiveOps and customer support gain a dashboard. On PlayFab projects, day-to-day operations typically run through a mix of raw PlayFab screens, internal scripts, ad-hoc tooling, and engineer-on-call requests. After the migration, the same workflows sit behind a single role-gated dashboard with audit logging on every action. This section walks the four surfaces that change: content authoring, LiveOps operations, customer support, and the admin API behind both. See Introduction to the LiveOps Dashboard for the feature overview.

Content authoring workflow

Design data — unit stats, currency costs, mail templates, IAP products, runtime tunables — moves from PlayFab Title Data into typed game-config libraries. Authoring happens in spreadsheets or CSV depending on what designers prefer; the build step packages the result into one content-hashed archive that the server and client both consume.

Surface Authoring path
Unit / mission / quest / gear definitions Spreadsheet or CSV (Phase 0 decision) → GameConfigLibrary<TKey, TInfo>
Currency and shop-item definitions Game-config libraries
Mail templates Game-config library plus optional per-broadcast slot fills
IAP products InAppProductInfoBase subclass in the IAP config library
Runtime-tunable knobs RuntimeOptions YAML on the server
Player segments PlayerCondition predicates in shared code; PlayerSegmentInfoBase entries in the segments library
Experiment variants GameConfigPatch per variant

The designer-direct vs engineer-mediated split depends on the authoring source. Spreadsheet- or CSV-driven libraries let designers ship a change without touching code; structurally new fields still need an engineer. RuntimeOptions edits sit in the deploy pipeline rather than the dashboard — they roll out via YAML, not click-to-publish — which matches what most teams want for the operational knob surface.

Operations (LiveOps Dashboard)

The dashboard owns the LiveOps workflows that on PlayFab live across several disconnected systems. The set:

  • Broadcasts – Targeted or global mail with reward attachments, scheduled in a window. Targeting uses the same PlayerCondition predicates as segments and push.
  • Push-notification campaigns – Authored alongside broadcasts; same predicate language; APNS and FCM delivered server-side.
  • LiveOps events – Time-limited activities — events, contests, login bonuses — bounded by a schedule window.
  • Experiments and A/B testing – Each variant is a GameConfigPatch; assignment is stored on the PlayerModel and persists across sessions.
  • Offers – LiveOps-configurable IAP through MetaOfferGroupInfoBase and dynamic-content adapters.
  • Player operations – Grant, ban, reset, view state, refund IAP — with the project's OnInAppProductRefunded override running game-side revocation.
  • Maintenance mode – Scheduled or immediate, with optional per-platform exclusions; the client receives a terminal error and a countdown during downtime.

The shift in operating posture is the headline: a LiveOps lead who on PlayFab filed a ticket and waited for an engineer can now ship the change directly, with the action recorded in the audit log.

Customer support

Customer support gains a per-player drill-down view and role-gated operations. Four roles ship out of the box — Game Admin, Game Viewer, CS Senior, CS Agent — with named permission bundles (see Managing Dashboard Permissions). Operations available per role include rename, ban, device reconnect, mail-with-attachments, event and login and incident and audit log access, and full GDPR export and deletion flows.

The day-to-day shape changes concretely:

  • Refunds – On PlayFab, an engineer runs a script to revoke a granted product and adjust the wallet. On Metaplay, CS clicks Refund in the dashboard; the project's refund override runs the game-side revocation; the action lands in the audit log.
  • Apology grants – On PlayFab, the team writes a one-off script that targets the affected player IDs and writes their save keys. On Metaplay, CS sends targeted mail with an attached reward from the dashboard — single player or a list — and the player receives it at next session start.
  • Account investigation – On PlayFab, CS asks engineering to pull and parse the player-data JSON. On Metaplay, CS opens the player view and reads the typed state directly; engineering stops being in the loop for routine questions.

There is no documented "log in as player" impersonation feature; the dashboard shows player state from the player's perspective but does not let CS act as the player. Teams that rely on impersonation on PlayFab should surface that requirement in Phase 0.

Admin API and scripting

Every dashboard operation is a signed REST call against the admin API. Per-endpoint [RequirePermission] guards apply the same role checks that the dashboard UI applies; external scripts call the same endpoints with the same auth surface. See Working with the LiveOps Dashboard HTTP API.

This is the foundation for CS tooling, automated LiveOps jobs, and test rigs that today live as one-off PlayFab scripts. The migration is the right moment to inventory those scripts and decide which ones move to dashboard operations, which become small admin-API clients, and which retire because the dashboard covers them natively. Bespoke CS tooling rarely survives the migration intact; most of it consolidates onto the dashboard.

Migration risk register

A short, honest list of what can go wrong during a PlayFab → Metaplay migration, with one-line mitigations. Each row is real; each row is bounded. The mitigations point back at the sections that carry the deeper treatment.

Risk Likelihood Impact Mitigation
Determinism drift between client and server on a ported helper Medium Medium The Roslyn analyzers and runtime checksum comparison catch most drift at build or first execution; the deterministic-helper checklist in the SharedCode section is the audit pass that closes the rest.
Save-data carry-over assumptions break against the new model Low Low Greenfield migrations start at schema version 1 with no live data to carry; soft-launched titles plan the carry-over explicitly with [MigrateFromVersion] methods on each changed type.
Hidden client coupling to a PlayFab-specific response code Medium Low The error-code mapping is its own work line — every project-wide refusal enum value maps to a MetaActionResult, audited at the call site rather than at the wrapper.
Photon plugin auth-handshake regression at cutover Low Medium Land the Metaplay ↔ Photon auth-bridge swap in the same window as the gameplay cutover; running the two halves out of phase breaks login during the transition.
IAP receipt-validation gap during per-store cutover Medium Medium Confirm shipping stores in Phase 0; allocate the platform-extension work for Amazon, Samsung, and any other non-default store before the cutover window opens.
Segment-evaluation semantic shift on day one Medium Low Sync server-side evaluation against the cached PlayFab behaviour catches the few rules that implicitly relied on the lag; document the change for the LiveOps team ahead of cutover.
LiveOps team unfamiliar with the dashboard at launch High Low Two short training passes during the migration window, plus a dashboard staging environment the team uses for real workflows before cutover — most teams pick it up in days.
Helper-class port inventory larger than the early estimate Medium Medium The reference timeline carries levers, not commitments; re-baseline once the actual helper count is known rather than at the planned end date.
Unbounded growth bug in player-data blobs ports as-is Medium Low Greenfield is the moment to fix; the gotcha section names the two replacement shapes (cap-in-place plus analytics, or rotating bounded window).
Day-one operational gaps in production clock control Low Low Time Skip is dev-only by default; surface any production-time-control requirement in Phase 0 so the gap is visible before testing assumes it is there.

None of these are migration-blockers. The combined risk surface is the price of moving off a constrained platform onto a model-first one, and each row above has a known shape on past projects.

Part 3: Subsystem-by-subsystem reference

A per-subsystem reference for a PlayFab → Metaplay migration. Each entry is self-contained — read the ones relevant to your project and skip the rest. These references are based on real migration projects we have worked on and cover the most common systems.

Authentication and session

A typical PlayFab project carries three pieces of auth code beyond the SDK itself:

  • An auth bootstrap class – Wraps PlayFab device or custom-ID login, plus social-link helpers for Google Play / Apple / Facebook recovery. Owns the session ticket and the player ID.
  • A session validator – The session ticket gets written into a read-only player-data key — typically called something like USER_SESSION_DATA_READ_ONLY — and the client polls the key periodically. Mismatch means another device has logged in; the client kicks itself. This is a soft anti-cheat: a banned or session-stolen player keeps playing until the next poll fires.
  • A custom HTTP retry plugin – Wraps the PlayFab SDK's HTTP layer to add retry-on-transient-failure, exponential backoff, and timeout handling that the stock SDK does not provide.

On Metaplay, the SDK ships social logins out of the box — device ID, Google Play Games, Sign In With Google, Sign In With Apple, Apple Game Center, Facebook, Steam, and more (see Getting Started with Social Logins). Social identities attach to a game account; the SDK exposes attachment, detachment, and conflict-resolution APIs. Single-session enforcement is automatic and immediate — when a new session opens for a player, the previous session is kicked. There is no polling window, no soft anti-cheat behaviour. The session connection is long-lived and persistent (WebSocket-style push), not a fresh HTTPS connection per call.

The work to land this:

  • Replace the auth bootstrap – The three custom classes collapse into the SDK's auth bootstrap plus a small amount of game-specific glue (welcome flow, attribution capture).
  • Map PlayFab error codes – The PlayFab SDK returns a handful of distinct refusals at login — account banned, linked-account-already-claimed, invalid endpoint, ticket-expired. Each maps to a Metaplay session-start refusal with the same user-facing UX.
  • Delete the polling validator – Both the read-only session key and the periodic poll go away. Metaplay enforces sessions on the server side at connection time.
  • Delete the retry plugin – Metaplay's transport handles retries, reconnects, and back-off; the project's bespoke retry wrapper around the PlayFab HTTP layer becomes unused code.

Save model: UserData to PlayerModel

A typical PlayFab project splits the player save across two key spaces, both backed by UserData-style dictionaries:

  • Read-only keys – Server-written, client-readable. Used for everything the server is authoritative on — wallet, inventory, progression, history. A mature game accumulates dozens of these.
  • User-data keys – Client-writable. Used for the state the client can mutate on its own — selected hero, last-seen news, dismissed badges, UI preferences. Smaller in count; typically a handful.

Each key is a JSON-serialised DTO. PlayFab caps UpdateUserData at ten keys per call, so save flows accumulate a chunking helper that groups mutations into batches of ten. Each key also has an effective size budget; large DTOs get manually split across multiple keys with a merge step at read time. The save layer typically carries two independent schema-version stamps (one per key space) and a migration runner that walks both on every login. JSON serialisation sits on the hot path of every save.

On Metaplay, one PlayerModel class replaces both key spaces. Every former save DTO becomes a [MetaSerializable]-annotated field on the player model with a stable [MetaMember(N)] tag. Persistence is a tagged binary format; the full model is serialised on each persist with no per-field dirty tracking. The full save fits in one operation, so the chunking and the manual splits both disappear. Schema versioning sits on annotations on the model class — one version per model, with field-level migration methods next to the changed field. See Entity Schema Versions and Migrations.

The work:

  • Annotate every save DTO – Add [MetaSerializable] and [MetaMember(N)]. Mechanical, but tag numbers stay with the type forever, so assign them deliberately on the first pass.
  • Remove JSON from the save path – The binary serializer replaces every JsonConvert call on the persist path. JSON stays for analytics-event export and ad-hoc tooling, but never for player state.
  • Collapse the read-only / user-data split – The split was a PlayFab permissions trick. On Metaplay, client-writability is at the action level, not the field level. Former user-data state — selected hero, dismissed badges, shop view state — becomes regular PlayerModel fields mutated by client-initiated actions.
  • Start at schema version 1 – Greenfield baseline; no history to carry. The legacy migration runner and the two legacy version stamps both delete outright.
  • Use deterministic collections on annotated fieldsMetaDictionary<K,V> and OrderedSet<T> replace Dictionary<K,V> and HashSet<T> on [MetaMember] fields. A Roslyn analyzer rejects the System.Collections.Generic equivalents at build time. DateTime is also rejected; use DateTimeOffset or MetaTime.
  • Delete the chunking helper and split-key merge – Both are PlayFab artefacts; Metaplay persists the whole model in one operation, and no field has a per-key size cap.

The save model is usually the lowest-risk port in the migration: mechanical, well-bounded, parallelisable across engineers, and caught at compile time by the analyzer suite.

Game definitions: Title Data to Game Configs

A typical PlayFab project keeps design data — unit stats, currency costs, drop rates, quest definitions, shop offers, mail templates — as JSON in PlayFab Title Data. The client fetches the whole set at startup, hash-caches it to disk, and reuses the cache on subsequent launches when the hash matches. The server reloads it per invocation. The shape that grows over time:

  • Per-domain keys – One Title Data key per definition domain — heroes, levels, gear, shop, mail templates. Typically a few dozen keys.
  • Manual splits – PlayFab caps each key at 1MB, so large maps get manually split across several keys with a merge step at read time.
  • Title-data hash plumbing – A separate Title Data key tracks the active version; the client compares hashes to decide whether to redownload.
  • An opaque ID type – Keys inside definition maps are usually 64-bit integers or custom GUID types — opaque to humans, fine for code, painful to debug or operate against.

On Metaplay, design data lives in strongly typed config libraries authored from spreadsheets or CSV. Builds trigger from the Unity Editor, the LiveOps Dashboard, or a custom menu item (see Working with Game Configs). Output is one binary archive, content-hashed, CDN-delivered. The archive splits into SharedGameConfig (client + server) and ServerGameConfig (server-only); individual fields can carry [ServerOnly] to be stripped from the client archive at build time. Libraries are strongly typed as GameConfigLibrary<TKey, TInfo> with StringId<T> keys; cross-references use MetaRef<T> or MetaConfigId<T> (see Game Config Item References). Keys are human-readable (starter_pack_v2, daily_quest_login), which makes the operations surface navigable rather than a wall of opaque integers.

The work:

  • Pick the source of truth – Spreadsheets if the designers prefer authoring there, CSV if the project already has an exporter from another tool. Status-quo authoring on top of a custom exporter also works. The runtime is equivalent either way; this is a designer-workflow call.
  • Migrate each definition domain – Each Title Data domain becomes one GameConfigLibrary<TKey, TInfo>. Internal title-data keys that backed mail templates, ban lists, or other built-in subsystems are mostly obsolete — covered in those subsystems' entries below.
  • Retype opaque IDs to StringId<T> – The migration is the right time to do this. With no live data to preserve, the cost is a scripted refactor across the call sites and the payoff is a navigable operations surface for the lifetime of the game.
  • Delete the title-data hash and split-key plumbing – The binary archive plus the content-hashed local cache replace both.
  • Move clock management to MetaTime – The server-time fetch helper, the client time-skew override, and the internal server-time-override title-data key all go. MetaTime is the single source of game time.

Hot reload semantics

A newly built game-config does not push to running sessions. A new session starts on the new archive; existing sessions keep the archive they were loaded with. This is intentional and matches what most LiveOps teams expect after switching off the PlayFab-style "any title-data change is instantly live" model.

RPC endpoints: Azure Functions to PlayerActions

A typical mature PlayFab project carries somewhere between a few dozen and well over a hundred server endpoints. The shape they share:

  • Reflection-dispatched routing – One front-door function looks up the endpoint name in a registry built at startup, finds the matching handler via reflection, and dispatches. Skips per-endpoint boilerplate at the cost of typed routing.
  • Per-call request DTOs – Each endpoint pairs with a client-side request DTO, often as a subclass of a project-wide retry-loop base. A few dozen of these accumulate.
  • Fan-out at the top of every handler – Each handler opens with a Task.WhenAll fetching the handful of save keys it needs plus the handful of definition maps it reads, then deserialises the JSON into typed shapes for the rest of the handler to work with.
  • A per-call error envelope – Every handler returns a wrapper type — typically AFResponse<T> or similar — with a result payload and an error code drawn from a project-wide enum that grows to around a hundred values over time.
  • Write-back at the end – The handler enumerates the keys it mutated and ships them back to PlayFab through the chunking helper.

On Metaplay, each endpoint becomes one PlayerAction class. The model is resident in the actor; the game config is loaded into a singleton. The save-key fetch and the definition fetch at the top of the handler both disappear — player.SomeField and player.GameConfig.SomeLibrary[id] are direct field accesses. The write-back at the end disappears too — Metaplay persists the mutated model automatically.

A typical breakdown of how endpoints port:

Endpoint category Work level Notes
Pure state mutations (FTUE, profile, settings, progression) Low Rename to Player…Action, move body to Execute(model, commit). Mechanical; AI-assisted.
Reward and inventory grants (gacha, shop, mission complete, battle-pass claim) High Touches RNG determinism and shared helper logic. Server-rolled action pattern lands here.
Cheats (grant currency, reset progression, unlock-all) Low Gain [DevelopmentOnlyAction] plus real auth. Production access via dashboard player ops.
Mail (claim, fetch, modify-state) Deleted Built-in actions replace; no game-side code needed.
Save bootstrap (create, delete) Deleted PlayerActor lifecycle replaces.
Post-login task runner Moved Becomes GameOnSessionStarted() and GameOnRestoredFromPersistedState() overrides on the player model.
Server-time fetch Deleted MetaTime replaces.
Scheduled dispatcher (mail, push, etc.) Deleted Broadcasts and push campaigns replace.

Error-code mapping is a non-trivial sub-workstream and deserves its own line in the plan. The project-wide error enum typically grows to around a hundred distinct codes with UX distinctions per category — "not enough currency" vs "max level reached" vs "feature locked" each rendered differently in the client. Each maps to a MetaActionResult and the client error-handling code rewires accordingly. Allocate this work explicitly; it does not amortise into the per-action port.

RNG-driven endpoints — gacha rolls, gear stat rolls, drop tables — get the server-rolled action treatment. The server issues an action with the random result baked into the action payload; the client replays the same code with the same inputs. Drift becomes impossible by construction. Use RandomPCG seeded from a server-supplied seed rather than System.Random, and the same Roslyn analyzers that catch determinism violations on the model catch them in shared helpers.

In-game mail system

A typical PlayFab project builds mail on top of the function host's durable-orchestrator capability. The shape that accumulates:

  • A durable orchestrator – Drives the mail send flow — fan out a broadcast to a player list, evaluate per-player eligibility, write the per-player mailbox state.
  • Per-player mailbox blobs – Each player's mailbox lives as a JSON document in blob storage, keyed by player ID. Read on mail fetch, written on mail send and on player claim.
  • Activities for the read/claim cycle – Server-side activities run the mail-template lookup, the eligibility check, the reward grant on claim. Client-facing endpoints sit on top — fetch mail, claim mail, mark-state.
  • A retry wrapper on segment fetch – PlayFab's segment API has rate limits; the orchestrator wraps GetPlayersInSegment with retries and back-off to push through the limits.
  • Mail templates as title-data – Mail content lives in an internal title-data key, separate from player-facing design data, often with its own state-tracking key.

On Metaplay, each PlayerModel carries a MailInbox of mail items with consumed / read / sent-at tracking and reward attachments (see Implementing In-Game Mail). Broadcasts target players by segment or explicit list with a schedule window; the LiveOps Dashboard is the authoring surface (see Managing Broadcasts). Reward attachments are MetaPlayerReward subclasses the game defines once and reuses across mail, missions, and offers.

Delivery is pull-based by design: each PlayerActor checks the active broadcast set at session start and periodically while online. Players who never log in during the broadcast window never receive the broadcast. This is intentional and matches what live games typically want — a player who returns after a month does not get flooded with backlog mail. Built-in actions cover add, consume, toggle-read, and delete; the game does not write its own.

The work:

  • Delete the orchestrator, the activities, the per-player blobs – These are pure deletion. The mailbox state moves onto the player model; the dispatch fan-out moves into the broadcast system; the segment-fetch retry wrapper goes with the segment subsystem (see Segments and targeting).
  • Move mail templates into a config library – Metaplay has no first-class template feature. Build a light indirection — a GameConfigLibrary<MailTemplateId, MailTemplateInfo> in SharedGameConfig — referenced from broadcast content. Designers author the template list; broadcasts pick a template by ID and optionally fill in slots.
  • Decide the targeted-mail flow – Individual player mail (rare LiveOps grants, CS apology grants) uses the dashboard send-mail endpoint or a single-player broadcast target list — both work; team preference governs.
  • Delete the mail-tracking title-data keys – The state-tracking key, the legacy template key, and the per-environment mail-monitoring queries go.

Targeted-mail and apology-mail flows that customer support runs today through a script become a dashboard action with audit logging attached. The CS team gets the operation; engineering stops being on call for it.

In-app purchases (IAP)

A typical PlayFab project structures IAP across four pieces:

  • Unity Purchasing as the storefront – Standard Unity Purchasing handles the platform-side storefront — initialise products, surface the platform UI, capture the receipt.
  • A studio storefront wrapper – A project-specific class wraps Unity Purchasing with the UI layer's expected API — buy buttons, confirmation modals, error dialogs, receipt-redelivery prompts.
  • PlayFab as the receipt-validation backend – The receipt goes to PlayFab; PlayFab validates with the platform and grants the product inventory.
  • An inventory redemption shim – A client-side helper reads the granted items out of PlayFab's inventory and maps them to in-game shop items, then writes those into the player save. Often paired with a read-only save key tracking which inventory grants have been redeemed already.

On Metaplay, the SDK owns the storefront layer end to end. Products are InAppProductInfoBase subclasses in a config library, each with per-platform store IDs and reward contents. The SDK validates receipts server-side for Google Play, Apple App Store, Steam, and Development out of the box; Amazon Appstore and Samsung Galaxy Store require a custom platform extension (see Getting Started with In-App Purchases). Dynamic purchase content resolves what a purchase grants at purchase time — used for LiveOps-configurable offers. Transactions and purchase history live on the player model; the game does not implement IStoreListener directly.

The work:

  • Delete the inventory-redemption shim – Both the shim and its read-only save key go. The product-grants-X mapping moves onto InAppProductInfoBase itself.
  • Bypass the studio storefront wrapper – The SDK's IAPManager owns the storefront. Audit every call site of the wrapper — purchase UI, confirmation screens, error dialogs, receipt-redelivery prompts, "your purchase failed" dialogs, restore-purchase flows — and rewire to the SDK's API. This is a real UI refactor, not a one-line swap.
  • Move the shop catalog into a config library – From PlayFab's store to GameConfigLibrary<InAppProductId, InAppProductInfoBase>. Catalog edits move onto the dashboard build flow.
  • Implement refund handling – Override OnInAppProductRefunded on the player model; the revocation logic — what to take back, what to leave, how to notify the player — is game-defined.
  • Plan platform extensions for non-default stores – Amazon and Samsung each need a custom InAppPurchasePlatform adapter. Scope per store.
  • Wire offers – LiveOps-configurable offers use MetaOfferGroupInfoBase plus MetaOfferInfoBase plus dynamic-content adapters (see Getting Started with In-Game Offers). The trade-off between hard-coded shop entries and dynamic-content offers is worth a deliberate call early.

Unity Purchasing version

Current Metaplay requires Unity Purchasing v4.x. Support for v5 lands in an upcoming SDK release. Projects already on v5 should hold for that release before cutting over; projects on v4 are fine.

Guilds

A typical PlayFab project builds guilds on top of PlayFab's Groups API and Data API:

  • Membership lifecycle via Groups API – Create, edit, destroy guilds. Join, leave, kick. Promote and demote between roles. Apply to a guild, accept and reject applications, transfer leadership. A mature project ends up with somewhere north of thirty async methods bridging Unity to the Groups API.
  • Per-guild metadata via Data API GetObjects – Guild description, settings, contribution totals, current state of any guild-wide progression. Read on every guild-screen open; written on the operations that change it.
  • Guild news / activity log as files – The Data API's GetFiles endpoint backs a per-guild news feed. Clients download the file over HTTP and render the entries. Long retention is cheap; structured queries are not.
  • Guild-versus-guild competition layered on top – A boss-rush or score-race cycle with per-guild leaderboard rows stored as direct strings, periodic resets driven by a scheduled Azure function, claimable rewards. Sits on top of the guild service rather than inside it.

On Metaplay, guilds are a server-driven entity with their own model, actions, and ticks — structurally a mirror of the player model. The SDK provides the framework; the game defines the model fields and the actions (see Guild Framework). Membership lifecycle, role permissions, search, recommendations, and guild transactions (atomic operations that touch both guild and player state) are framework-provided. Custom guild-wide state — contributions, scores, settings, members' shared progress — becomes annotated fields on the guild model.

The work:

  • Replace the guild service plumbing – The PlayFab Groups + Data calls collapse into the Metaplay guild client API. The IGuildService interface stays; the implementation swaps. Method count drops sharply because most of the old service was PlayFab call composition rather than game logic.
  • Move guild metadata onto the guild model – Description, settings, contribution totals, progression state — each becomes a [MetaSerializable] field. The per-call GetObjects round trip disappears; the client gets the guild model on session start and on mutation.
  • Pick news-feed storage – Short-bounded history (last N events) lives on the guild model directly. Long-retention logs go to server-side blob storage with a small action that lists and reads. Most games are fine with the bounded option.
  • Build the GvG cycle on guild-actor ticks – The scheduled-reset Azure function becomes a tick on the guild actor or a Metaplay scheduled server action. Per-guild leaderboards live on the guild model for in-guild rankings; cross-guild leaderboards overlap with the leagues primitive — see Async PvP and leaderboards.
  • Delete the file-backed news plumbing and the HTTP download path – Pure deletion once the storage decision lands.

Preview status and gaps

The guild framework is currently shipped as a preview feature — safe to use in production, but a handful of operations that PlayFab projects routinely lean on are not yet framework-provided and need game-side implementation: join applications and the accept-or-reject flow, banning, hiding invite-only guilds from search, and the guild news and leaderboard storage discussed above. Project the scope accordingly; the work is bounded but not zero.

Async PvP and leaderboards

A typical PlayFab project carries async PvP — the player fights a stored defense team, not a live opponent — as a dedicated subsystem. The shape:

  • Matchmaking and battle flow in Azure Functions – Refresh opponents, fetch the current opponent list, start a battle, complete a battle, set the defense team, claim arena rewards, update tickets. A PvPArenaService on the client wraps the lot; mature projects accumulate 10–15 endpoints here.
  • Honor / rating tracked through PlayFab data – The per-player rating lives in PlayFab UserData or PlayerStatistics. Weekly decay and weekly seasonal reset run from a scheduled Azure function that walks the player base.
  • Leaderboards in PlayFab statistics or a custom store – The Azure layer reads a rank window around each player on demand. Globally-sorted leaderboards scale awkwardly past a point and are typically the slowest screen in the live game.
  • Per-arena config blobs in title-data – Refill rates, rank thresholds, honor decay curves, weekly reset schedule, matchmaking parameters — each as its own title-data key, edited through the PlayFab console.

On Metaplay, the async-PvP shape lands on three primitives:

  • Player state on PlayerModel – Defense team, honor / rating, ticket counts, claim state, season membership — all annotated fields on the player model. Atomic mutations happen inside player actions.
  • Matchmaking via the async matchmaker – The SDK ships an async matchmaker that selects opponents from a pool keyed on a configurable bucket (rating band, honor range, level bracket). The matchmaker is a server-side actor; the per-player call is one action that returns the picked opponents. See Integrating the Async Matchmaker.
  • Leaderboards via Leagues and Divisions – Players are allocated to a rank (Bronze, Silver, Gold, …) and sharded into divisions inside each rank for the duration of a season. Each division is a disposable single-use entity. End-of-season promotion and demotion is framework-provided. See Leagues Quick Guide.

The work:

  • Move per-player arena state onto the player model – The bookkeeping that used to live across UserData, PlayerStatistics, and stat custom-store rows collapses into a handful of fields. Weekly decay and reward claiming become player actions.
  • Stand up the async matchmaker – Define the bucket key (commonly honor band or rating). Migrate refresh-opponents and get-opponents endpoints onto matchmaker queries.
  • Move leaderboards onto Leagues and Divisions – Carry the existing rank brackets across as the league's rank thresholds. The global ranked ladder that the PlayFab implementation typically provides is intentionally not the Metaplay shape — division banding is what scales, and the framework's promotion/demotion handles season turnover. Plan the client UI around division-local rankings rather than global rank-N readouts.
  • Move the weekly cycle onto model ticks – Honor decay, season rollover, and ticket refill all move onto PlayerModel ticking or a Metaplay scheduled server action. The scheduled Azure function that walked the player base retires.
  • Move arena configs into SharedGameConfig – Refill rates, rank thresholds, decay curves, matchmaking parameters — each becomes a strongly-typed config entry. The per-config title-data keys go.

Hybrid is a valid call for PvP

Of all the subsystems in this reference, the async-PvP stack is the one where a partial migration is sometimes the right answer. Keeping the existing Azure matchmaking and leaderboard layer and bridging it into the Metaplay server — reading defense teams from the player model, deducting tickets through player actions, granting rewards through player actions — avoids a full rewrite of matchmaking and leaderboard infrastructure. The tradeoff is one remaining Azure dependency for a single subsystem. Worth weighing explicitly when the existing PvP infra is mature.

Player segments and targeting

A typical PlayFab project carries one segment-evaluator helper plus two known workarounds:

  • Client-side segment read – Used for shop gating and feature gating in the UI. Calls GetPlayerSegments and caches the result for the session.
  • Server-side segment fetch with retry wrapper – The mail orchestrator and any offer-targeting logic call GetPlayersInSegment. PlayFab rate-limits this API, so the call sits behind a Polly-style retry wrapper that pushes through the limits with back-off.
  • Segment definitions in the PlayFab console – Segment predicates are authored in PlayFab's web UI, not in code. There is no source-of-truth in the repo; what segments exist and what they match is vendor-console state.

On Metaplay, segments are config-library entries wrapping a PlayerCondition predicate. The SDK ships a basic condition type for property-range checks; custom predicates are game-defined and live in shared code (see Implementing Player Segments). Evaluation is server-side per player, in memory, with no rate limits. The same predicates power broadcasts, push campaigns, experiments, dynamic offers, and any game-side code that needs to ask "does this player match this segment?".

The work:

  • Re-implement the existing segments – Each PlayFab segment becomes a PlayerCondition subclass plus a PlayerSegmentInfoBase entry. Authoring moves from the PlayFab console into the repo; the segments live next to the rest of the design data.
  • Delete the segment-evaluator helper and the retry wrapper – Both are PlayFab-specific. Local evaluation has no rate limit to dodge.
  • Rewire client-side gatinggameConfig.PlayerSegments[segmentId].MatchesPlayer(playerModel) replaces the cached client-side segment-list read. Evaluation is local on the mirrored player model.

Behavioural change

PlayFab evaluates segment membership asynchronously and caches it. A player who just qualified for a segment may not see it for a few minutes. On Metaplay, segment membership is evaluated against the current player model at the moment of the question, with no cache. Code that implicitly relied on the lag (rare, but it exists) will see the new behaviour and usually prefer it.

Analytics and telemetry

A typical PlayFab project keeps analytics and telemetry split across two systems:

  • Service telemetry into AppInsights – Each server-side function call emits dependency telemetry — start time, duration, success / failure, endpoint name. Used to drive operational dashboards; not designed for product analytics.
  • Bridging app to a downstream APM – A separate (often TypeScript) function app runs Kusto queries against AppInsights on a schedule, transforms the results, and pushes to the downstream APM (Datadog being the common choice). Service-level visibility lives there.
  • Empty product analytics on the client – The client typically has an analytics-manager class with event-name constants — but most of the time the calls are stubbed and PlayStream is not wired up. Product teams have to ask analytics-engineering for any per-event question.

On Metaplay, product analytics is first-class. Events are typed PlayerEventBase subclasses defined in shared code, with a type-code attribute. Events emit from inside actions, with optional game-defined enrichment — player context, labels, derived properties. See Implementing Analytics Events. Built-in export sinks cover JSON-to-S3 and BigQuery; other destinations need a custom sink (see Streaming Analytics Events to External Storage). Server metrics are exposed on a Prometheus endpoint at /metrics; Grafana dashboards are pre-configured in Private Cloud.

The work:

  • Design the event schema – Nothing to preserve on the client side — the existing constants point at unwired code. Treat this as a greenfield event-schema exercise with product and analytics engineering. Each event becomes a typed class in shared code.
  • Route service telemetry – The Prometheus endpoint replaces the AppInsights dependency telemetry. Existing operational dashboards either reconnect to Prometheus directly or are recreated against the Metaplay-shipped Grafana dashboards.
  • Pick the event-sink path – Out of the box, BigQuery is the cheap path for a data warehouse; an APM-style sink needs a few days of custom-sink work. Both is a valid answer if product and operations have different questions.
  • Retire the bridging app – The TypeScript Kusto-to-APM app stops being load-bearing once the service telemetry routes via Prometheus and the product events route via the new sink.

Event emission from inside actions has a useful property: it is deterministic, which means tests can assert on the emitted events the same way they assert on state mutations. The migration is the right time to write the first round of analytics tests rather than treating analytics as a fire-and-forget side effect.

LiveOps features

A typical PlayFab project has no LiveOps surface to speak of:

  • Maintenance flags in a separate vendor – Usually Unity Gaming Services Remote Config or similar. A separate SDK, a separate credential set, a separate dashboard.
  • No A/B testing – Experiments are out of scope; if they happen at all, they happen via build-time feature flags shipped per-client.
  • No broadcasts or push campaigns – Both live in third-party services (push) or as bespoke server orchestration (mail).
  • No signed admin API or CS roles – The team accesses production data through raw PlayFab screens or one-off scripts. No audit trail, no role separation, no GDPR workflow.

On Metaplay, the LiveOps surface lands as a single dashboard with all of it built in:

  • Maintenance mode – Scheduled or immediate, with optional per-platform exclusions; the client receives a terminal error and a countdown during downtime (docs).
  • Time skip – Forward-only MetaTime offset for dev and local testing of time-based features. Disabled in staging and production; useful for testing daily-reset, weekly-event, and battle-pass progression flows quickly.
  • Logic-version gating – Compiled and dashboard-configurable client version ranges. Raising the active minimum forces older clients off without a server downtime.
  • RuntimeOptions – Server-side tunables read from YAML files watched on disk, with hot-reload for non-static options. The dashboard surfaces current values read-only; edits happen in YAML and roll out via the deploy pipeline.
  • Experiments and A/B testing – Each variant is a GameConfigPatch. At session start the server applies stacked patches to build a variant-specialised config for that player. Assignment is stored on the player model and persists across sessions.
  • Localization – Per-language archives delivered via CDN independently of the game config. Translators do not need to be in the design-data build loop.
  • Signed admin API and CS roles – Per-endpoint [RequirePermission] guards. Named role bundles — Game Admin, Game Viewer, CS Senior, CS Agent (see Managing Dashboard Permissions). Every admin action carries an audit trail.

The work:

  • Migrate the maintenance flag – The single remote-config flag in scope today becomes Metaplay's MaintenanceMode. The remote-config SDK dependency drops out of the Unity project.
  • Define RuntimeOptions for runtime knobs – Anything else the team currently keeps in the external service — feature toggles, kill switches, soft-launch flags — becomes a typed RuntimeOptions subclass with a YAML-edited value.
  • Wire experiments even without a day-one experiment – The infrastructure costs little to stand up and the LiveOps team will want it on day one of live operations. Cheaper to set up alongside the migration than retrofit.
  • Audit role assignments – The migration is the right time to define who needs Game Admin, who needs CS Senior, who only needs Game Viewer. Project the existing access patterns onto the role model.

The dashboard becomes the operations surface — broadcasts, push campaigns, experiments, dynamic offers, player ops, maintenance mode, audit logs, GDPR workflows. Most LiveOps work that today requires a code change or an engineer-run script moves onto it.

Push notifications

A typical PlayFab project uses a third-party push provider wired into the Unity project with an Android plugin config and a vendor-side console for authoring campaigns. The shape:

  • Provider SDK in the client – One Unity package, one Android plugin config block, one initialisation call at startup. Devices register their push tokens with the provider.
  • Campaign authoring in the vendor console – LiveOps authors push campaigns in the provider's UI, targets players by tags pushed up from the client.
  • A separate subscription cost – Push providers price by subscriber count and send volume — a meaningful direct cost line for a live game.

On Metaplay, push notification campaigns are first-class LiveOps features. Devices register their push tokens with the Metaplay server; the server delivers via APNS and FCM. Campaigns are authored in the LiveOps Dashboard and target players by the same PlayerCondition predicates that power segments, broadcasts, and experiments. See Implementing Push Notifications and Managing Push Notifications.

The work:

  • Remove the provider SDK – The Unity package, the Android plugin config, and the startup initialisation call all go.
  • Wire APNS and FCM credentials into Metaplay – One configuration step in the operations setup. Mobile credentials move from the vendor's console into Metaplay's secret management.
  • Move campaigns onto the dashboard – Existing campaigns get recreated against the unified targeting (segments instead of vendor tags). Once cut over, the provider subscription retires.
  • Decide the cutover timing – Cutover-together with the backend migration is cleaner; a parallel soak reduces risk for games with heavy push-driven return traffic.

The win beyond cost retirement is unified targeting — a single predicate language drives push, mail, experiments, and offers, so a campaign that says "players in the comeback segment" means the same thing across all four channels.

Cheat endpoints

A typical PlayFab project accumulates a set of cheat endpoints during development — grant currency, reset progression, unlock-all, skip-tutorial, set-level. The hazard:

  • Open by default – The endpoints typically sit alongside production endpoints in the same handler registry, with no server-side role check between them. A common TODO comment near the dispatcher reads something like "we should check for cheat calls here".
  • Reachable by any client – Without a server-side check, any client can call the cheat endpoints in production by knowing their names — which often leak through decompiled clients or in error messages.
  • Indistinguishable from production endpoints – The reflection-dispatch routing treats them as ordinary handlers. The only thing keeping them benign is that most clients do not call them.

On Metaplay, dev actions carry the [DevelopmentOnlyAction] attribute and the server refuses them in production unless the player is explicitly flagged as a developer. See Implementing Development-Only Features. For production LiveOps and customer-support use cases — grant currency, reset progression, ban a player — the LiveOps Dashboard exposes role-gated player operations with audit logging.

The work is mechanical: mark each cheat action with the attribute, delete the now-dead authorisation TODO, and route the production-operations use cases that some cheats covered onto the dashboard's player-ops surface. The pre-existing authorisation hole closes as a side effect of the migration.

Planning a PlayFab migration?

If you're running a live game on PlayFab and weighing the move, the fastest way to scope it is a conversation against your actual subsystem inventory — the levers in the reference timeline section map directly onto a scoping call. Get a demo and we'll walk your stack against the patterns in this guide, or check pricing for how Metaplay engagements are structured.

If you want to explore the platform first: the Metaplay docs cover everything referenced here, the docs MCP server lets your AI assistant query them directly, and Metaplay AI covers the AI-assisted development tooling that compresses the mechanical phases of a migration.

FAQ

How long does a PlayFab to Metaplay migration take?

It depends on the subsystem footprint, not a fixed schedule. A small project — a handful of gameplay subsystems, a few dozen server endpoints, vanilla integrations — lands in a few weeks. A medium project with loadouts, quests, battle pass, mail, segments, and push in scope takes a couple of months. A large project with a custom multiplayer integration, bespoke mail orchestration, and over a hundred endpoints takes a quarter or more. The drivers are endpoint count, helper-class inventory, which integration gotchas are present, team shape, and how much AI-assisted leverage the team uses.

Do I have to rewrite my Unity client?

No — in practice the client side is a retarget, not a rewrite. Most mature PlayFab Unity projects already route UI through a backend façade that fires C# events, which is exactly the shape Metaplay's client uses. The strategy is to keep the events and replace what fires them: façade methods collapse from async HTTP wrappers to one-line ExecuteAction calls, and the transport, retry, queueing, and DTO layers delete outright. A typical project sheds several thousand lines of client code on the net.

What replaces Azure Functions in a Metaplay migration?

Each server endpoint becomes a typed PlayerAction class in shared code — the same business logic without the function-host plumbing, PlayFab I/O, JSON serialization, or error envelopes. Pure state mutations port mechanically (and compress well with AI assistance); reward and gacha endpoints use the server-rolled action pattern; mail, save bootstrap, server-time, and scheduled-dispatcher endpoints delete entirely because Metaplay provides them.

What happens to live player data during the migration?

Greenfield and soft-launch migrations start at schema version 1 with no history to carry — the lowest-risk shape. Titles with live player data to preserve plan the carry-over explicitly using Metaplay's [MigrateFromVersion] field-level migration methods. Either way, the PlayFab save-key split, the chunking helpers, and the legacy migration runner are replaced by one typed PlayerModel class with tagged binary persistence.

Can I keep Photon Quantum when moving to Metaplay?

Yes. Photon continues to handle real-time multiplayer and matchmaking. The migration touches two integration points: the auth handshake (Metaplay mints the session claim the Photon plugin verifies, replacing the PlayFab ticket flow) and match-result validation (a Metaplay match-result entity validates cross-player agreement and grants rewards server-side). Both are solved patterns; plan the auth swap to land in the same window as the gameplay cutover.

How much code gets deleted in a PlayFab migration?

Across client and server combined, a mature PlayFab project typically sheds 15-20k lines on the net, against 6-10k added. The deletions concentrate in transport, persistence, and orchestration: request queues and retry loops, per-endpoint DTOs, save-key chunking, title-data caching and split-key merges, mail orchestrators, the reflection-dispatch router, and the inventory redemption shim. The game logic that remains is the part worth keeping.

Does Metaplay support self-hosting?

Yes — two options. Metaplay Cloud is the managed offering: faster to stand up, no operational toil, and the recommended starting point for most studios. Private Cloud is self-hosted on AWS (provisioned via Metaplay's Terraform and Helm modules; GCP and Azure are not supported) and suits studios with compliance, data-residency, or cost-at-volume requirements. Starting managed and moving to self-host later is supported — the runtime is identical.

What LiveOps features do I gain that PlayFab doesn't have?

A single role-gated dashboard with broadcasts and reward mail, push campaigns, A/B experiments with per-variant config patches, dynamic offers, player segments evaluated without rate limits, maintenance mode, audit-logged player operations, GDPR export and deletion workflows, and named CS roles. On a typical PlayFab project these either don't exist, live in third-party vendors, or require an engineer running scripts.