Authentication & Account Linking¶
Authentication is the process of verifying a player's identity and loading their profile. Balancy provides a flexible authentication system that supports multiple scenarios, enabling cloud save functionality, seamless cross-device access, and social integration.
Key APIs:
Balancy.API.Auth.*- Authentication methods (login/switch accounts)Balancy.API.Link.*- Account linking methods (connect auth to current account)
Important Distinction:
- Use
API.Authwhen you want to authenticate/load a specific account (login, switch accounts) - Use
API.Linkwhen you want to connect an authentication method to the current account (link email to current account)
Authentication Scenarios¶
1. Automatic Authentication (Default)¶
By default, Balancy authenticates players using deviceId, allowing them to:
- Store their progress in the cloud
- Restore their progress later on the same device (if
deviceIddoesn't change)
This authentication method is invisible to the player, requiring no developer intervention. Everything is handled by Balancy automatically and under the hood.
Pros:
- No additional implementation required
- Seamless experience with automatic cloud saving and restoring
- Ideal for casual games with no account linking requirements
Cons:
- Players may lose progress if they change devices or
deviceIdchanges after the app is reinstalled - No cross-platform or multi-device support unless manually linked to another authentication method
When to Use:
- Casual games where friction-free onboarding is critical
- Games targeting users who don't want to create accounts
- Prototypes and testing
2. Forced Authentication at Game Start¶
Some games require players to authenticate before loading their profile using a method like:
- Email & Password
- Apple ID
- Other third-party authentication providers
In this case:
- The game displays a login screen at the start.
- The user must authenticate before Balancy loads their profile.
- The authentication token is cached for future sessions, allowing automatic login.
- Until authentication is complete, only game content is loaded, but the user profile is not.
Pros:
- Ensures player progress is always recoverable
- Allows seamless cross-platform progression
- Prevents issues with lost
deviceId
Cons:
- Requires players to create or log in to an account before playing
- Potential friction for new players who just want to try the game
When to Use:
- Games with cross-platform support
- Games with significant player investment (time, money)
- Competitive games requiring verified identities
- Games with strong social features
Authentication Flow Diagrams¶
Initial Authentication Flow¶
┌──────────────────────────────────────────────────────────────┐
│ Client: Authenticate Request │
│ - deviceId (automatic) │
│ - OR custom credentials (name/password, social token) │
└────────────────┬─────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Server: Check if Auth Exists │
└────────┬──────────────────────────────┬──────────────────────┘
│ │
┌────▼────┐ ┌────▼────┐
│ No Auth │ │ Auth │
│ Exists │ │ Exists │
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Create New │ │ Force Link │
│ Account │ │ Device Id │
└────┬────────────┘ └────┬────────────┘
│ │
▼ │
┌─────────────────┐ │
│ Connect the │◄────────────────┘
│ Network │
└────┬────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Return Success │
│ - User Id │
└────────────────┬─────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ SDK: Load Profiles │
│ - System Profile (LiveOps data) │
│ - Custom Profiles (game data) │
└──────────────────────────────────────────────────────────────┘
Key Points:
- New users: Account is created automatically
- Returning users: Existing account is loaded
- Profile loading: Only happens after successful authentication
Account Linking Flow¶
┌──────────────────────────────────────────────────────────────┐
│ Client: Link Request │
│ - Current token (from active session) │
│ - Network credentials (email/password, social token) │
│ - forceLink flag (conflict resolution) │
└────────────────┬─────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Server: Validate Token │
└────────┬─────────────────────────────────────┬───────────────┘
│ │
┌────▼────┐ ┌────▼────┐
│ Token │ │ No │
│ Valid │ │ Token │
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌──────────────────────┐ ┌─────────────────────┐
│ Check if Binding │ │ Disconnect Network │
│ Already Exists │ │ from Old Bind │
└────┬──────────┬──────┘ └────┬────────────────┘
│ │ │
┌───▼───┐ ┌──▼───┐ │
│ Yes │ │ No │ │
└───┬───┘ └──┬───┘ │
│ │ │
▼ │ │
┌─────────────────────┐ │
│ Same Account? │ │
│ (token.user_id == │ │
│ bind.user_id) │ │
└───┬──────────┬──────┘ │
│ │ │
┌───▼───┐ ┌───▼───┐ │
│ Yes │ │ No │ │
└───┬───┘ └───┬───┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Return Fail │ │
│ │ (Conflict!) │ │
│ └──────────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ Connect the │◄───────────────────┘
│ Network │◄───────────────────┐
└────┬────────────┘ │
│ │
▼ │
┌──────────────────────────────────────┴───────────────────────┐
│ Return Success │
│ - User Id │
└────────────────┬─────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ SDK: Handle Response │
│ - Check if UserId changed (account switched) │
│ - If forceLink → Load Cloud Profiles → Force Link Device Id │
│ → Delete Local Account & Activate Cloud Account │
└──────────────────────────────────────────────────────────────┘
Key Points:
- Binding conflict: Occurs when the network credential is already linked to a different account
- forceLink flag: When
true, unlinks from the old account and links to current account - Account switching: When linking to a different account, local progress is replaced with cloud data
API Reference¶
Authentication Methods¶
Guest Authentication¶
Authenticate as a guest using automatic device ID.
using Balancy;
using Balancy.Core.Responses;
public void AuthenticateAsGuest()
{
API.Auth.AsGuest((AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log("Authenticated as guest successfully!");
Debug.Log($"User ID: {response.UserId}");
// Profile is automatically loaded
}
else
{
Debug.LogError($"Authentication failed: {response.ErrorMessage}");
Debug.LogError($"Error Code: {response.ErrorCode}");
}
});
}
When to Call:
- At game start if using automatic authentication
- When player clicks "Play as Guest" button
- When recovering from a failed custom authentication
Response Fields:
Success(bool): Whether authentication succeededUserId(string): Unique user identifierErrorCode(int): Error code if failedErrorMessage(string): Human-readable error description
Name & Password Authentication¶
Authenticate using email/username and password.
using Balancy;
using Balancy.Core.Responses;
public void AuthenticateWithCredentials(string name, string password)
{
API.Auth.WithNameAndPassword(name, password, (AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log("Authenticated successfully!");
Debug.Log($"User ID: {response.UserId}");
// Profile is automatically loaded
OnAuthenticationSuccess();
}
else
{
Debug.LogError($"Authentication failed: {response.ErrorMessage}");
// Handle specific errors
if (response.ErrorCode == 401)
{
ShowError("Invalid credentials");
}
else if (response.ErrorCode == 404)
{
ShowError("Account not found. Please register.");
}
else
{
ShowError($"Authentication error: {response.ErrorMessage}");
}
}
});
}
Parameters:
name(string): Email address or usernamepassword(string): User passwordcallback(ResponseCallback): Response handler
Common Error Codes:
401: Invalid credentials404: Account not found500: Server error
Account Linking Methods¶
Link with Name & Password¶
Link an email/password authentication method to the current account.
using Balancy;
using Balancy.Core.Responses;
public void LinkEmailPassword(string email, string password, bool forceLink = false)
{
// Store current user ID before linking
var currentUserId = Balancy.Profiles.System?.UserId;
API.Link.WithNameAndPassword(email, password, forceLink, (LinkResponseData response) =>
{
if (response.Success)
{
Debug.Log("Account linking operation completed!");
// Check if account switched by comparing user IDs
if (response.UserId == currentUserId)
{
Debug.Log("Linked to current account");
ShowNotification("Email linked successfully!");
}
else
{
Debug.Log("Switched to different account");
Debug.Log($"New User ID: {response.UserId}");
// Profile is automatically reloaded
OnAccountSwitched();
}
}
else
{
Debug.LogError($"Linking failed: {response.ErrorMessage}");
// Handle binding conflict
if (response.ErrorCode == 409) // Conflict
{
ShowBindingConflictDialog(email);
}
else
{
ShowError($"Linking error: {response.ErrorMessage}");
}
}
});
}
Response Fields:
Success(bool): Whether linking succeededUserId(string): User ID (may change if account was switched)ErrorCode(int): Error code if failedErrorMessage(string): Human-readable error description
forceLink Behavior:
| forceLink | Binding Conflict? | Result |
|---|---|---|
false |
No | Links to current account |
false |
Yes | Returns error (409 Conflict) |
true |
No | Links to current account |
true |
Yes (same account) | Links to current account |
true |
Yes (different account) | Unlinks from old account, switches to that account |
Note: To detect if the account switched, compare the response.UserId with the user ID before the linking operation.
Common Error Codes:
409: Binding conflict (credentials already linked to another account)400: Invalid credentials format500: Server error
Complete Integration Examples¶
Example 1: Guest Start with Optional Login¶
using UnityEngine;
using Balancy;
using Balancy.Core.Responses;
public class AuthenticationManager : MonoBehaviour
{
private void Start()
{
// Check if we have a cached token
// Balancy handles this automatically during Init
// For explicit guest auth (optional):
AuthenticateAsGuest();
}
private void AuthenticateAsGuest()
{
API.Auth.AsGuest((AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log("Playing as guest");
OnAuthenticationComplete();
}
else
{
Debug.LogError($"Guest auth failed: {response.ErrorMessage}");
ShowErrorScreen();
}
});
}
// Called when player clicks "Login" in settings
public void ShowLoginDialog()
{
// Display login UI
LoginUI.Show(OnLoginSubmit);
}
private void OnLoginSubmit(string email, string password)
{
API.Auth.WithNameAndPassword(email, password, (AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log("Logged in successfully!");
OnAuthenticationComplete();
}
else
{
ShowLoginError(response.ErrorMessage);
}
});
}
private void OnAuthenticationComplete()
{
// Profiles are automatically loaded by Balancy
LoadMainMenu();
}
}
Example 2: Forced Login at Start¶
using UnityEngine;
using Balancy;
using Balancy.Core.Responses;
public class ForcedAuthManager : MonoBehaviour
{
[SerializeField] private GameObject loginScreen;
[SerializeField] private GameObject gameScreen;
private void Start()
{
// Check if user is already authenticated
if (IsAlreadyAuthenticated())
{
// Balancy automatically restores session during Init
// Wait for profiles to load
WaitForProfileLoad();
}
else
{
// Show login screen
ShowLoginScreen();
}
}
private bool IsAlreadyAuthenticated()
{
// Balancy handles session persistence automatically
// Check if profile is available after Init
return Balancy.Profiles.System != null;
}
private void ShowLoginScreen()
{
loginScreen.SetActive(true);
gameScreen.SetActive(false);
}
public void OnLoginButtonClicked(string email, string password)
{
ShowLoadingIndicator();
API.Auth.WithNameAndPassword(email, password, (AuthResponseData response) =>
{
HideLoadingIndicator();
if (response.Success)
{
Debug.Log("Login successful!");
OnLoginSuccess();
}
else
{
ShowLoginError(response.ErrorMessage);
}
});
}
private void OnLoginSuccess()
{
loginScreen.SetActive(false);
gameScreen.SetActive(true);
// Profiles are automatically loaded
InitializeGame();
}
public void OnLogoutButtonClicked()
{
// Reset Balancy session
Balancy.Profiles.Reset();
// Return to login screen
ShowLoginScreen();
}
}
Example 3: Account Linking in Settings¶
using UnityEngine;
using Balancy;
using Balancy.Core.Responses;
public class AccountLinkingManager : MonoBehaviour
{
public void ShowLinkEmailDialog()
{
LinkEmailUI.Show(OnLinkEmailSubmit);
}
private void OnLinkEmailSubmit(string email, string password)
{
ShowLoadingIndicator();
// First attempt without forceLink
API.Link.WithNameAndPassword(email, password, false, (LinkResponseData response) =>
{
HideLoadingIndicator();
if (response.Success)
{
Debug.Log("Email linked successfully!");
ShowNotification("Email linked to your account!");
RefreshAccountSettings();
}
else if (response.ErrorCode == 409) // Binding conflict
{
Debug.Log("Binding conflict detected");
ShowBindingConflictDialog(email, password);
}
else
{
ShowError($"Linking failed: {response.ErrorMessage}");
}
});
}
private void ShowBindingConflictDialog(string email, string password)
{
// Show dialog with two options
ConflictDialog.Show(
message: "This email is already linked to another account. What would you like to do?",
option1: "Unlink & Link to Current Account",
option2: "Load the Other Account",
onOption1: () => UnlinkAndLinkToCurrent(email, password),
onOption2: () => LoadOtherAccount(email, password)
);
}
private void UnlinkAndLinkToCurrent(string email, string password)
{
// Store current user ID
var currentUserId = Balancy.Profiles.System?.UserId;
// This keeps the current account and just links the email
API.Link.WithNameAndPassword(email, password, true, (LinkResponseData response) =>
{
if (response.Success && response.UserId == currentUserId)
{
Debug.Log("Email linked to current account!");
ShowNotification("Email linked successfully!");
RefreshAccountSettings();
}
else if (response.Success)
{
Debug.Log("Account was switched instead of linked");
OnAccountSwitched();
}
else
{
ShowError($"Linking failed: {response.ErrorMessage}");
}
});
}
private void LoadOtherAccount(string email, string password)
{
// Warn about progress loss
ConfirmDialog.Show(
message: "Loading the other account will replace your current progress. Continue?",
onConfirm: () =>
{
// Use Auth to switch to the other account
API.Auth.WithNameAndPassword(email, password, (AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log("Switched to other account");
Debug.Log($"New User ID: {response.UserId}");
// Profiles are automatically reloaded
OnAccountSwitched();
}
else
{
ShowError($"Account switch failed: {response.ErrorMessage}");
}
});
}
);
}
private void OnAccountSwitched()
{
// Reload the game with new account data
ShowNotification("Account switched successfully!");
// Refresh all UI
RefreshAllGameUI();
// Optionally return to main menu
LoadMainMenu();
}
}
Managing Authentication in Settings¶
Regardless of the authentication scenario, players may need to manage their accounts from the Settings menu.
1. Linking Additional Authentication Methods¶
Players should be given an option to link additional authentication methods (e.g., Facebook, Google, Apple ID).
✅ UI Implementation:
- Show a list of available authentication methods.
- If a method is already linked, show a
Unlinkbutton. - If a method is not linked, show a
Linkbutton. - (Optional) Provide rewards for linking new authentication methods.
✅ Handling Conflicts:
If the player tries to link a new authentication method that is already linked to another account, a conflict arises.
Conflict Resolution Popup:
-
Unlink & Link to Current Account
- The authentication method is unlinked from the previous account.
- It is now linked to the player’s current session.
- The player continues playing without interruption.
-
Load the Other Account
- The account associated with the authentication method is loaded.
- The player's local progress may be lost unless linked to another method.
Best Practice:
Before allowing a switch, warn the player about potential progress loss.
2. Changing Accounts¶
Case 1: Automatic Authentication (Device ID)¶
For games using Balancy's automatic authentication, include a "Change Account" or "Load Account" button.
✅ Flow:
- The player clicks "Change Account".
- A popup appears showing available authentication methods (Facebook, Google, etc.).
- The player selects a method and logs in.
-
If an account is found:
- The game loads the new account.
- The local progress is lost.
-
If no account is found:
- The player is asked: "No account found. Do you want to create a new account?"
- The developer can provide an option to create a new account.
✅ Warning Players About Progress Loss:
If the local account has no linked authentication methods, show a warning before switching accounts.
Case 2: Forced Authentication (Login Required at Start)¶
For games that require login at startup, instead of a "Change Account" button, provide a "Log Out" button.
✅ Flow:
- The player clicks "Log Out".
- The game returns to the login screen.
- The player must re-authenticate before continuing.
- No gameplay is allowed until authentication is complete.
This method ensures strict account control, preventing unauthorized or accidental progress overwrites.
Best Practices¶
1. Authentication Persistence¶
✓ Good:
// Balancy automatically handles authentication persistence
// No manual implementation needed
API.Auth.WithNameAndPassword(email, password, callback);
// Session is automatically maintained
❌ Bad:
// Don't manually save/manage authentication state
PlayerPrefs.SetString("user_id", response.UserId); // Not needed!
Why: Balancy handles authentication persistence internally and automatically maintains the session across app restarts.
2. Handling Binding Conflicts¶
✓ Good:
API.Link.WithNameAndPassword(email, password, false, (response) =>
{
if (response.Success)
{
// Success - same account
ShowSuccess();
}
else if (response.ErrorCode == 409)
{
// Conflict - ask user what to do
ShowConflictDialog();
}
});
❌ Bad:
// Don't always use forceLink=true
API.Link.WithNameAndPassword(email, password, true, callback);
// This can cause unexpected account switches!
Why: Always start with forceLink=false to detect conflicts. Only use forceLink=true after user confirmation.
3. Warn Before Progress Loss¶
✓ Good:
private void SwitchAccount()
{
ConfirmDialog.Show(
"Switching accounts will replace your current progress. Continue?",
onConfirm: () => PerformAccountSwitch()
);
}
❌ Bad:
// Don't switch accounts without warning
API.Link.WithNameAndPassword(email, password, true, callback);
// User loses progress unexpectedly!
Why: Players should always be warned before potentially losing progress.
4. Handle All Error Cases¶
✓ Good:
API.Auth.WithNameAndPassword(email, password, (response) =>
{
if (response.Success)
{
OnSuccess();
}
else
{
switch (response.ErrorCode)
{
case 401:
ShowError("Invalid credentials");
break;
case 404:
ShowError("Account not found");
break;
case 500:
ShowError("Server error. Please try again.");
break;
default:
ShowError($"Error: {response.ErrorMessage}");
break;
}
}
});
❌ Bad:
API.Auth.WithNameAndPassword(email, password, (response) =>
{
if (response.Success)
{
OnSuccess();
}
// No error handling!
});
Why: Always handle errors gracefully and provide meaningful feedback to users.
5. Loading Indicators¶
✓ Good:
public void Login(string email, string password)
{
ShowLoadingIndicator("Logging in...");
API.Auth.WithNameAndPassword(email, password, (response) =>
{
HideLoadingIndicator();
if (response.Success)
{
OnSuccess();
}
else
{
ShowError(response.ErrorMessage);
}
});
}
Why: Authentication involves network calls that may take time. Always show loading state.
Common Patterns¶
Pattern: Progressive Authentication¶
// Start with guest auth for quick onboarding
void Start()
{
API.Auth.AsGuest(OnGuestAuthComplete);
}
// Later, encourage linking
void OnPlayerLevel5()
{
ShowNotification("Link your account to save progress across devices!");
ShowLinkAccountButton();
}
Use Case: Casual games that want quick onboarding but encourage account creation later.
Pattern: Forced Auth with Guest Fallback¶
void Start()
{
if (HasCachedCredentials())
{
AutoLogin();
}
else
{
ShowLoginScreen(allowGuestMode: true);
}
}
public void OnPlayAsGuestClicked()
{
API.Auth.AsGuest((response) =>
{
if (response.Success)
{
ShowNotification("Playing as guest. Progress may be lost!");
StartGame();
}
});
}
Use Case: Games that prefer login but allow guest mode for trying the game.
Troubleshooting¶
Problem: "Authentication failed: Session expired"¶
Causes:
- Session expired on server
- Account was deleted or modified
- Authentication state corrupted
Solutions:
// Clear Balancy state and re-authenticate
Balancy.Profiles.Reset();
API.Auth.AsGuest(callback); // Or custom auth
Problem: "Binding conflict" error even with forceLink=true¶
Causes:
- Network request failed mid-process
- Server-side conflict resolution failed
Solutions:
// Retry with forceLink
API.Link.WithNameAndPassword(email, password, true, (response) =>
{
if (!response.Success)
{
Debug.LogError($"Retry failed: {response.ErrorMessage}");
// Contact support or try different credentials
}
});
Problem: Lost progress after linking account¶
Causes:
- Linked to a different account without warning
- Used
forceLink=trueon a conflict
Prevention:
// Always check if UserId changed
var currentUserId = Balancy.Profiles.System?.UserId;
API.Link.WithNameAndPassword(email, password, forceLink, (response) =>
{
if (response.Success)
{
if (response.UserId == currentUserId)
{
Debug.Log("Linked to current account - progress safe");
}
else
{
Debug.Log("Switched to different account - progress replaced");
// Should have warned user before this!
}
}
});
Security Considerations¶
1. Never Log Passwords¶
❌ Bad:
Debug.Log($"Logging in with password: {password}"); // NEVER DO THIS!
✓ Good:
Debug.Log($"Logging in with email: {email}");
// Don't log password at all
2. Secure Session Management¶
✓ Good:
// Balancy handles session security automatically
// Authentication state is managed by the SDK
❌ Bad:
// Don't implement custom session storage
PlayerPrefs.SetString("session_id", someId); // Not secure!
Note: Balancy automatically manages secure session storage. Trust the SDK's built-in security mechanisms.
3. Validate Input¶
✓ Good:
public void Login(string email, string password)
{
if (string.IsNullOrWhiteSpace(email))
{
ShowError("Email is required");
return;
}
if (!IsValidEmail(email))
{
ShowError("Invalid email format");
return;
}
if (string.IsNullOrWhiteSpace(password))
{
ShowError("Password is required");
return;
}
if (password.Length < 6)
{
ShowError("Password must be at least 6 characters");
return;
}
API.Auth.WithNameAndPassword(email, password, callback);
}
Summary of Best Practices¶
| Scenario | Best Practice |
|---|---|
| No Custom Authentication (Device ID) | No action required; progress is stored automatically. |
| Forced Authentication (Login required) | Authenticate before loading the profile; cache token for future sessions. |
| Linking Authentication Methods | Allow linking/unlinking; resolve conflicts with "Unlink & Link" or "Load Another Account". |
| Changing Accounts (Auto Auth) | Provide a "Change Account" button with authentication options and a progress loss warning. |
| Changing Accounts (Forced Auth) | Use a "Log Out" button that forces the player to log in again. |
Following these best practices ensures seamless authentication, data security, and a better player experience across all platforms.