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)
Authentication vs Linking¶
Understanding the difference between these two operations is critical.
Authentication (API.Auth) |
Linking (API.Link) |
|
|---|---|---|
| Purpose | "I am this person" | "Also remember this credential" |
| Account | Switches to the account owning the credentials | Keeps the current account |
| Profile | Replaces current profile with new one | Profile unchanged |
| Device | Device relinked to new account | Device unchanged |
| Use case | Logging in on a new device | Adding email to save progress |
Warning
Using API.Auth with credentials belonging to a different account will switch accounts and replace the current profile. Use API.Link when you want to add an authentication method without switching accounts.
Visual Comparison¶
BEFORE: AFTER AUTH (WithNameAndPassword "alice"):
┌──────────────┐ ┌──────────────┐
│ Account_A │ │ Account_B │ ◄── SWITCHED
│ device: dev1 │ │ name: alice │
│ level: 5 │ │ device: dev1 │ ◄── device relinked
└──────────────┘ │ level: 42 │ ◄── different profile
└──────────────┘
BEFORE: AFTER LINK (WithNameAndPassword "alice"):
┌──────────────┐ ┌──────────────┐
│ Account_A │ │ Account_A │ ◄── SAME account
│ device: dev1 │ │ device: dev1 │
│ level: 5 │ │ name: alice │ ◄── network added
└──────────────┘ │ level: 5 │ ◄── same profile
└──────────────┘
User Account States¶
The SDK recognizes three states based on the current session:
┌─────────────┐
│ SIGNED OUT │
│ userId=null│
└──────┬──────┘
│
┌────────────┼────────────┐
│ continueAsGuest() │ authBy*()
▼ ▼
┌────────────────┐ ┌────────────────┐
│ ANONYMOUS │ │ LINKED │
│ userId exists │ │ userId exists │
│ networks=[] │ │ networks=[..] │
└───────┬────────┘ └───────┬────────┘
│ │
│ linkBy*() │ signOut()
▼ ▼
┌────────────────┐ ┌────────────────┐
│ LINKED │ │ SIGNED OUT │
└────────────────┘ └────────────────┘
| State | Description |
|---|---|
| Signed Out | No active session. User must authenticate. |
| Anonymous | Authenticated by device only. Progress is at risk if the device is lost. |
| Linked | At least one recoverable auth method (email, username, social) linked. Account is safe. |
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.
To implement this, set autoLogin to false in your config:
var config = new AppConfig {
ApiGameId = "your-game-id",
PublicKey = "your-public-key",
AutoLogin = false, // Disable auto-login
};
Balancy.Main.Init(config);
const config = new AppConfig({
apiGameId: 'your-game-id',
publicKey: 'your-public-key',
autoLogin: false, // Disable auto-login
});
Balancy.Main.init(config);
When autoLogin is false, the SDK loads CMS data but does not authenticate. You must call an Auth.* method manually.
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
API Reference¶
Authentication Methods¶
Guest Authentication¶
Authenticate as a guest using automatic device ID.
API.Auth.AsGuest((AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log($"Guest session: {response.UserId}");
}
else
{
Debug.LogError($"Auth failed: {response.ErrorMessage}");
}
});
// No asGuest() in TypeScript.
// Guest/device authentication is handled automatically when autoLogin: true (default).
// With autoLogin: false, call one of the named auth methods instead.
Note
There is no asGuest() in the TypeScript API. Guest/device authentication is handled automatically when autoLogin: true.
Name & Password Authentication¶
Authenticate using a username and password. If the credentials belong to a different account, the SDK switches to that account.
API.Auth.WithNameAndPassword(name, password, (AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log($"Signed in as {response.UserId}");
}
else
{
Debug.LogError($"Auth failed: {response.ErrorMessage}");
}
});
Balancy.API.Auth.withNameAndPassword(name, password, (response) => {
if (response.success) {
console.log(`Signed in as ${response.userId}`);
} else {
console.error(`Auth failed: ${response.errorMessage}`);
}
});
Email & Password Authentication¶
Authenticate using an email address and password.
API.Auth.WithEmailAndPassword(email, password, (AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log($"Signed in as {response.UserId}");
}
else
{
Debug.LogError($"Auth failed: {response.ErrorMessage}");
}
});
Balancy.API.Auth.withEmailAndPassword(email, password, (response) => {
if (response.success) {
console.log(`Signed in as ${response.userId}`);
} else {
console.error(`Auth failed: ${response.errorMessage}`);
}
});
Social Provider Authentication¶
Authenticate with Apple, Google, or Facebook. These methods require a userId and token obtained from the native OAuth flow on your platform.
// Apple
API.Auth.WithApple(appleUserId, appleToken, callback);
// Google
API.Auth.WithGoogle(googleUserId, googleToken, callback);
// Facebook
API.Auth.WithFacebook(facebookUserId, facebookToken, callback);
// Apple
Balancy.API.Auth.withApple(appleUserId, appleToken, callback);
// Google
Balancy.API.Auth.withGoogle(googleUserId, googleToken, callback);
// Facebook
Balancy.API.Auth.withFacebook(facebookUserId, facebookToken, callback);
Note
OAuth providers require native platform integration to obtain tokens. The SDK does not handle the OAuth flow itself.
Get Account Info¶
Retrieve the current user's linked authentication methods.
API.Auth.GetInfo((UserInfoResponseData response) =>
{
if (response.Success)
{
Debug.Log($"User: {response.UserId}");
// NetworksJson is a JSON string: [{"kind":"device","value":"xxx"}, ...]
Debug.Log($"Networks: {response.NetworksJson}");
}
});
Balancy.API.Auth.getInfo((response) => {
if (response.success) {
const networks = JSON.parse(response.networksJson);
console.log(`Linked methods: ${networks.map(n => n.kind).join(', ')}`);
}
});
Sign Out¶
Sign out the current user. All profile-dependent API calls will return null/error until the user re-authenticates.
API.Auth.SignOut((ResponseData response) =>
{
if (response.Success)
{
Debug.Log("Signed out");
// Show login screen
}
});
Balancy.API.Auth.signOut((response) => {
if (response.success) {
console.log('Signed out');
// Show login screen
}
});
Unlink Authentication Methods¶
Remove an authentication method from the current account.
// Unlink username
API.Auth.UnlinkName("alice", (ResponseData response) =>
{
if (response.Success)
Debug.Log("Username unlinked");
});
// Unlink email
API.Auth.UnlinkEmail("alice@example.com", (ResponseData response) =>
{
if (response.Success)
Debug.Log("Email unlinked");
});
// Unlink username
Balancy.API.Auth.unlinkName('alice', (response) => {
if (response.success)
console.log('Username unlinked');
});
// Unlink email
Balancy.API.Auth.unlinkEmail('alice@example.com', (response) => {
if (response.success)
console.log('Email unlinked');
});
Tip
After unlinking, call GetInfo() to refresh the list of linked networks.
Account Linking Methods¶
Link with Name & Password¶
Link a username/password authentication method to the current account.
API.Link.WithNameAndPassword(name, password, forceLink, (LinkResponseData response) =>
{
if (response.Success)
{
Debug.Log($"Linked to {response.UserId}");
}
else if (response.ErrorCode == 409)
{
Debug.Log("Binding conflict - credentials already linked to another account");
}
else
{
Debug.LogError($"Link failed: {response.ErrorMessage}");
}
});
Balancy.API.Link.withNameAndPassword(name, password, forceLink, (response) => {
if (response.success) {
console.log(`Linked to ${response.userId}`);
} else if (response.errorCode === 409) {
console.log('Binding conflict');
} else {
console.error(`Link failed: ${response.errorMessage}`);
}
});
Link with Email & Password¶
API.Link.WithEmailAndPassword(email, password, forceLink, (LinkResponseData response) =>
{
if (response.Success)
Debug.Log("Email linked");
});
Balancy.API.Link.withEmailAndPassword(email, password, forceLink, (response) => {
if (response.success)
console.log('Email linked');
});
Link with Social Providers¶
API.Link.WithApple(appleUserId, appleToken, forceLink, callback);
API.Link.WithGoogle(googleUserId, googleToken, forceLink, callback);
API.Link.WithFacebook(facebookUserId, facebookToken, forceLink, callback);
Balancy.API.Link.withApple(appleUserId, appleToken, forceLink, callback);
Balancy.API.Link.withGoogle(googleUserId, googleToken, forceLink, callback);
Balancy.API.Link.withFacebook(facebookUserId, facebookToken, forceLink, callback);
forceLink Behavior¶
The forceLink parameter controls what happens when credentials already belong to another account.
| forceLink | Credential free? | Credential on other account? | Result |
|---|---|---|---|
false |
Yes | -- | Link succeeds |
false |
-- | Yes | Returns error (409 Conflict) |
true |
Yes | -- | Link succeeds |
true |
-- | Yes | Unlinks from old account, links to current account |
Warning
With forceLink=true, credentials are silently unlinked from the other account. The other account's owner will no longer be able to log in with those credentials.
Callbacks¶
OnSignedOut¶
Called when the user is signed out, either by calling SignOut() or due to a session conflict.
Balancy.Callbacks.OnSignedOut = () =>
{
Debug.Log("User signed out - show login screen");
ShowLoginScreen();
};
Balancy.Callbacks.onSignedOut.subscribe(() => {
console.log('User signed out');
showLoginScreen();
});
Session Conflicts¶
When the same account logs in from another device, the current session is invalidated:
Device A: Playing as alice Device B: Logs in as alice
│ │
│ ◄──── WebSocket: conflict_login ──── │
│ │
▼ │
Session invalidated: │
├── All managers cleared │
├── WebSocket disconnected │
└── OnDisconnected fires │
(AnotherSessionConflict) │
│ │
▼ ▼
Device A: Must re-authenticate Device B: Active session
Handle this via the OnDisconnected callback:
Balancy.Callbacks.OnDisconnected += reason =>
{
if (reason == Balancy.Callbacks.DisconnectReason.AnotherSessionConflict)
{
ShowMessage("Your account was logged in on another device.");
ShowLoginScreen();
}
};
Balancy.Callbacks.OnDisconnected.subscribe((reason) => {
if (reason === DisconnectReason.AnotherSessionConflict) {
showErrorDialog('Another session detected. Please log in again.');
}
});
Custom Auth Override¶
For full control over the authentication flow, use Actions.Auth to intercept the auth moment and provide your own login UI.
SDK Init
│
▼
autoLogin=false? ──yes──▶ CMS loads ──▶ AuthRequired notification
│
Actions.Auth registered?
│ │
yes no
│ │
▼ ▼
CustomAuthCallback() (developer calls
fires — developer Auth.* when ready)
shows own login UI
and calls Auth.*
// Set up before Init:
Balancy.Actions.Auth.SetCustomAuthCallback(() =>
{
// SDK says "I need auth now" - show your own login UI
MyLoginUI.Show(onComplete: (username, password) =>
{
Balancy.API.Auth.WithNameAndPassword(username, password, response =>
{
if (response.Success)
Debug.Log("Authenticated!");
else
Debug.LogError(response.ErrorMessage);
});
});
});
var config = new AppConfig {
AutoLogin = false,
};
Balancy.Main.Init(config);
// Set up before init:
Balancy.Actions.Auth.setCustomAuthCallback(() => {
showLoginScreen((email, password) => {
Balancy.API.Auth.withEmailAndPassword(email, password, (response) => {
if (response.success)
console.log('Authenticated!');
else
console.error(response.errorMessage);
});
});
});
const config = new AppConfig({
autoLogin: false,
});
Balancy.Main.init(config);
User Journey Flows¶
Journey 1: New User -- Guest to Linked Account¶
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: First Launch │
│ │
│ autoLogin=true → SDK calls authByDeviceId("device-abc") │
│ Server: creates Account_A, device linked │
│ State: ANONYMOUS (device-only) │
│ networks: [{kind:"device", value:"device-abc"}] │
└──────────────────────────────┬──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: User Decides to Save Progress │
│ │
│ Developer calls: API.Link.WithNameAndPassword("alice", │
│ "secret", forceLink: true, callback) │
│ Server: links name "alice" to Account_A │
│ State: LINKED │
│ networks: [{kind:"device"}, {kind:"name", value:"alice"}] │
└──────────────────────────────┬──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: User Gets New Phone │
│ │
│ New device, autoLogin=true → authByDeviceId("device-xyz") │
│ Server: creates Account_B (new device, no link) │
│ State: ANONYMOUS │
└──────────────────────────────┬──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 4: User Logs In to Existing Account │
│ │
│ Developer calls: API.Auth.WithNameAndPassword("alice", │
│ "secret", callback) │
│ Server: authenticates as Account_A, relinks device-xyz │
│ SDK: userId changed → swap profile data │
│ State: LINKED (Account_A on new phone) │
│ │
│ Note: Account_B is now orphaned (device-xyz was relinked) │
└─────────────────────────────────────────────────────────────────┘
Journey 2: Sign Out and Switch Account¶
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: User is signed in as alice │
│ │
│ State: LINKED │
│ networks: [{kind:"device"}, {kind:"name", value:"alice"}] │
└──────────────────────────────┬──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: User Signs Out │
│ │
│ API.Auth.SignOut(callback) │
│ SDK: clears tokens, fires OnSignedOut │
│ State: SIGNED OUT │
│ All profile API calls return null/error │
└──────────────────────────────┬──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: User Logs In as Different Person │
│ │
│ API.Auth.WithNameAndPassword("bob", "password", callback) │
│ Server: authenticates as bob's account │
│ State: LINKED (bob's profile loaded) │
└─────────────────────────────────────────────────────────────────┘
Complete Integration Examples¶
Example 1: Guest Start with Optional Login¶
public class AuthenticationManager : MonoBehaviour
{
private void Start()
{
AuthenticateAsGuest();
}
private void AuthenticateAsGuest()
{
API.Auth.AsGuest((AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log("Playing as guest");
LoadMainMenu();
}
else
{
Debug.LogError($"Guest auth failed: {response.ErrorMessage}");
}
});
}
// Called when player clicks "Login" in settings
public void OnLoginSubmit(string email, string password)
{
API.Auth.WithNameAndPassword(email, password, (AuthResponseData response) =>
{
if (response.Success)
{
Debug.Log("Logged in successfully!");
LoadMainMenu();
}
else
{
ShowLoginError(response.ErrorMessage);
}
});
}
}
// With autoLogin: true (default), the SDK authenticates as guest automatically.
// No explicit guest auth call needed.
// Login when player clicks "Login" in settings:
function onLoginSubmit(email: string, password: string) {
Balancy.API.Auth.withNameAndPassword(email, password, (response) => {
if (response.success) {
console.log('Logged in successfully!');
loadMainMenu();
} else {
showLoginError(response.errorMessage);
}
});
}
Example 2: Forced Login at Start¶
public class ForcedAuthManager : MonoBehaviour
{
private void Start()
{
Balancy.Actions.Auth.SetCustomAuthCallback(() =>
{
ShowLoginScreen();
});
var config = new AppConfig
{
ApiGameId = "your-game-id",
PublicKey = "your-public-key",
AutoLogin = false,
};
Balancy.Main.Init(config);
}
public void OnLoginButtonClicked(string email, string password)
{
ShowLoadingIndicator();
API.Auth.WithNameAndPassword(email, password, (AuthResponseData response) =>
{
HideLoadingIndicator();
if (response.Success)
InitializeGame();
else
ShowLoginError(response.ErrorMessage);
});
}
public void OnLogoutButtonClicked()
{
API.Auth.SignOut(response =>
{
if (response.Success)
ShowLoginScreen();
});
}
}
Balancy.Actions.Auth.setCustomAuthCallback(() => {
showLoginScreen();
});
const config = new AppConfig({
apiGameId: 'your-game-id',
publicKey: 'your-public-key',
autoLogin: false,
});
Balancy.Main.init(config);
function onLoginButtonClicked(email: string, password: string) {
showLoadingIndicator();
Balancy.API.Auth.withEmailAndPassword(email, password, (response) => {
hideLoadingIndicator();
if (response.success)
initializeGame();
else
showLoginError(response.errorMessage);
});
}
function onLogoutButtonClicked() {
Balancy.API.Auth.signOut((response) => {
if (response.success)
showLoginScreen();
});
}
Example 3: Account Linking in Settings¶
public class AccountLinkingManager : MonoBehaviour
{
private void OnLinkEmailSubmit(string email, string password)
{
ShowLoadingIndicator();
// First attempt without forceLink to detect conflicts
API.Link.WithNameAndPassword(email, password, false, (LinkResponseData response) =>
{
HideLoadingIndicator();
if (response.Success)
{
ShowNotification("Email linked to your account!");
}
else if (response.ErrorCode == 409)
{
ShowBindingConflictDialog(email, password);
}
else
{
ShowError($"Linking failed: {response.ErrorMessage}");
}
});
}
private void ShowBindingConflictDialog(string email, string password)
{
ConflictDialog.Show(
message: "This email is already linked to another account.",
option1: "Unlink & Link to Current Account",
option2: "Switch to Other Account",
onOption1: () =>
{
API.Link.WithNameAndPassword(email, password, true, response =>
{
if (response.Success)
ShowNotification("Email linked!");
});
},
onOption2: () =>
{
API.Auth.WithNameAndPassword(email, password, response =>
{
if (response.Success)
OnAccountSwitched();
});
}
);
}
}
function onLinkEmailSubmit(email: string, password: string) {
showLoadingIndicator();
// First attempt without forceLink to detect conflicts
Balancy.API.Link.withEmailAndPassword(email, password, false, (response) => {
hideLoadingIndicator();
if (response.success) {
showNotification('Email linked to your account!');
} else if (response.errorCode === 409) {
showBindingConflictDialog(email, password);
} else {
showError(`Linking failed: ${response.errorMessage}`);
}
});
}
function onForceLinkClicked(email: string, password: string) {
Balancy.API.Link.withEmailAndPassword(email, password, true, (response) => {
if (response.success)
showNotification('Email linked!');
});
}
function onSwitchAccountClicked(email: string, password: string) {
Balancy.API.Auth.withEmailAndPassword(email, password, (response) => {
if (response.success)
onAccountSwitched();
});
}
Managing Authentication in Settings¶
1. Linking Additional Authentication Methods¶
Players should be able to link additional authentication methods from a settings screen.
UI Implementation:
- Show a list of available authentication methods.
- If a method is already linked, show an
Unlinkbutton. - If a method is not linked, show a
Linkbutton. - Use
GetInfo()to fetch the current list of linked networks.
Handling Conflicts:
If the player tries to link credentials that are already linked to another account, a 409 Conflict error is returned (when forceLink=false). Present a dialog with options:
- Unlink & Link to Current Account -- Keeps current progress, reassigns the credential.
- Switch to Other Account -- Uses
API.Authto switch accounts (current progress is replaced).
Warning
Always warn players about potential progress loss before switching accounts.
2. Changing Accounts¶
For games using automatic authentication, include a "Change Account" button. For games using forced authentication, provide a "Log Out" button that calls API.Auth.SignOut() and returns to the login screen.
Best Practices¶
1. Authentication Persistence¶
Balancy handles authentication persistence internally. Do not manually save or manage authentication state.
2. Handling Binding Conflicts¶
Always start with forceLink=false to detect conflicts. Only use forceLink=true after user confirmation.
3. Warn Before Progress Loss¶
Always warn players before switching accounts, as their current progress will be replaced.
4. Handle All Error Cases¶
Always handle both success and error in authentication callbacks. Provide meaningful feedback to users.
5. Loading Indicators¶
Authentication involves network calls. Always show loading indicators during auth operations.
6. Reload Account Info After Link/Unlink¶
After a successful link or unlink, call GetInfo() to refresh the list of linked networks.
Common Patterns¶
Pattern: Progressive Authentication¶
Start with guest auth for quick onboarding, then encourage linking later:
void Start()
{
API.Auth.AsGuest(OnGuestAuthComplete);
}
void OnPlayerReachesLevel5()
{
ShowNotification("Link your account to save progress across devices!");
ShowLinkAccountButton();
}
// autoLogin: true handles guest auth automatically
function onPlayerReachesLevel5() {
showNotification('Link your account to save progress across devices!');
showLinkAccountButton();
}
Troubleshooting¶
Problem: "Auth failed" on startup¶
- Check that
apiGameIdandpublicKeyare correct. - Check network connectivity.
- If
autoLogin: false, ensure you call anAuth.*method after theAuthRequirednotification fires or inside the custom auth callback.
Problem: User's progress resets after reinstall¶
The device ID changes on reinstall. The old device-only account becomes unreachable. Solution: Encourage users to link a username or email early.
Problem: GetInfo() returns empty networks¶
The user is in the Anonymous state (device-only). The device network is implicit and may not appear in the networks array.
Problem: Unexpected logout / session conflict¶
Another device logged in with the same account. Handle the OnDisconnected callback with AnotherSessionConflict reason.
Problem: forceLink: false returns error 409¶
The credential belongs to another account. Ask the user whether to force-link or switch accounts.
Security Considerations¶
Never Log Passwords¶
// BAD - Never do this
Debug.Log($"Password: {password}");
// GOOD - Only log non-sensitive info
Debug.Log($"Logging in with email: {email}");
Trust the SDK's Session Management¶
Balancy automatically manages secure session storage. Do not implement custom session storage using PlayerPrefs or similar.
Validate Input¶
Validate email format and password length before calling auth methods.
Summary¶
| Scenario | Best Practice |
|---|---|
| Automatic Authentication (Device ID) | No action required; progress is stored automatically. |
| Forced Authentication (Login required) | Set autoLogin: false, use custom auth callback or show login screen. |
| Linking Authentication Methods | Allow linking/unlinking; resolve conflicts with dialog. |
| Changing Accounts (Auto Auth) | Provide a "Change Account" button with progress loss warning. |
| Changing Accounts (Forced Auth) | Use SignOut() + login screen. |
See Also¶
- SDK Initialization -- SDK setup and
autoLoginconfig - Configs & Profiles -- Accessing player data after authentication
- SDK API Reference -- Other API methods