signInAnonymously() on every app launch that doesn't have an existing session. This is the foundation everything else builds on. Ship the game with just this and you have a functional auth system.
Anonymous-first authentication with silent Game Center, cascading to Apple/Google/Email. Technical architecture, funnel strategy, and implementation guide for a mid-casual iOS game with a 2GB download.
Anonymous-first + silent Game Center is the optimal flow. Supabase creates an anonymous session at launch (zero UI). Game Center authenticates silently in the background for most iOS users. If Game Center fails, the user plays anonymously through the tutorial, then sees a "Save your progress?" prompt offering Apple/Google/Email sign-in. The anonymous Supabase user_id persists across all transitions—no data migration ever.
Game Center is not a native Supabase provider. You bridge it via a Supabase Edge Function that verifies Apple's cryptographic signature and issues a JWT. For Godot, GodotApplePlugins (SwiftGodot/GDExtension by Miguel de Icaza) provides Game Center + Sign in with Apple out of the box for Godot 4.2+.
The goal of any mobile game auth system is to maximize the number of users who reach gameplay while preserving the ability to identify, retain, and recover accounts. Every interaction between "app launch" and "playing the game" is a leak in the funnel. The math is unforgiving:
| Metric | Industry benchmark | Implication |
|---|---|---|
| Login screen abandonment | ~20% of users | 1 in 5 users quit at a login wall |
| Form abandonment (registration) | ~92% of consumers have abandoned | Never show a form before gameplay |
| Day 1 retention (iOS games) | 27–35% | You lose 2/3 of users on Day 1 |
| Day 7 retention | 10–15% | Compounding loss every day |
| Day 30 retention | 3–6% | Only the deeply engaged remain |
Your 2GB download is the first filter. Users who completed that download are already invested—they waited, they have intent. Hitting them with a login wall at this point is the worst possible friction. The download itself is your commitment device. Auth should be invisible on first launch.
Supercell (Clash of Clans, Brawl Stars): Zero login at launch. Device-bound account. Supercell ID offered later via Settings as a passwordless email code system for cross-device sync and account recovery. Supercell ID is presented as a save/sync feature, not a login gate.
Marvel Snap: Shows Guest / Google / Apple / Facebook options on the title screen, but Guest is the default and most prominent option. Account linking is prompted after the tutorial via Settings. Progress is preserved regardless of which path the user takes.
Apple Arcade titles: Game Center silently and automatically. No login screen at all. This is the gold standard for iOS.
Parents playing with kids changes the calculus. A parent handing their phone to a child does not want that child entering an email address or Apple ID password. Game Center is perfect here because it's already authenticated at the device level—the child doesn't interact with auth at all. Anonymous mode is the fallback: no PII, no forms, no risk. This also helps with COPPA compliance (covered in Section 6).
The system operates in two phases. Phase 1 (launch, zero UI) creates an anonymous Supabase session and attempts silent Game Center auth. Phase 2 (post-tutorial) prompts remaining anonymous users to link an identity provider.
Supercell's auth evolution is instructive because they went through exactly the transition you're designing for.
Originally (2012-2016): Clash of Clans used Game Center on iOS and Google Play Games on Android as the sole account identity. Your village was tied to your Game Center ID. If you switched Game Center accounts, you could load a different village. This was the only recovery mechanism. If you didn't have a Game Center account or forgot which Apple ID you used, Supercell support had to manually intervene.
Supercell ID (2017+): Supercell built their own cross-platform identity system. It's a passwordless email login: enter your email, receive a 6-digit code, done. No password to remember. Once connected, Supercell ID replaces Game Center as the account identity. You can no longer load your account via Game Center after connecting. This was a deliberate choice: Supercell wanted platform-independent identity so users could move between iOS and Android.
First launch: Zero login. You get a device-bound account immediately and start playing. No Game Center prompt, no sign-in screen, nothing. Supercell generates an internal player ID and stores the session locally.
Supercell ID prompt: After some gameplay (varies by title), a small banner or settings option encourages you to connect Supercell ID. It's not mandatory. Many players play for months without connecting. The prompt framing is about protecting your progress and playing on multiple devices, not about "creating an account."
Game Center relationship: Supercell games still integrate with Game Center for leaderboards and achievements, but Game Center is no longer used as an authentication mechanism for account recovery since Supercell ID replaced it. If you haven't connected Supercell ID and lose your device, Supercell support can sometimes recover your account using Game Center data as a secondary verification method, but it's not guaranteed.
Game Center is automatically enabled on every iOS device with an Apple ID. You sign into Game Center by signing into your Apple ID, which happens during device setup. Apple aggressively auto-signs users back into Game Center even when they try to sign out (this is a consistent complaint in Apple Support forums). The practical result: the vast majority of iOS devices have Game Center active.
One indie developer reported ~59% of game launches had Game Center authenticated. However, this data is from 2020 and the measurement methodology was imprecise. The real number for modern iOS (15+) is likely higher, because Game Center is now tied to the system Apple ID and Apple makes it very difficult to permanently disable. Conservative estimate: 70-85% of your iOS users will have Game Center available at launch. The remaining 15-30% are users who have actively signed out of Game Center (rare), have restricted accounts (Family Sharing child accounts with GC disabled by parents), or are on very old iOS versions.
For the users who don't have Game Center, the cascade catches them: anonymous first, then the post-tutorial prompt for Apple/Google/Email. No user falls through every net.
This is the core tension: creatures are the product. If a user loses their creatures, they've lost everything that makes Bobium Brawlers personal. The auth system exists primarily to prevent this.
| User state | Deletes app | New phone, same Apple ID | New phone, different Apple ID |
|---|---|---|---|
| Anonymous only (no GC, no Apple, no Google) | Creatures gone forever. No recovery path. | Creatures gone. New anonymous account. | Creatures gone. |
| Game Center linked | Reinstall, GC auto-auths, Edge Fn finds user by teamPlayerID, session restored. |
Same. GC identity travels with Apple ID. | Different Apple ID = different teamPlayerID. No match. |
| Apple Sign-In linked | Reinstall, sign in with Apple, Supabase matches by Apple sub ID. | Same Apple ID = same sub. Works. | Different Apple ID = different sub. |
| Google linked | Reinstall, sign in with Google, Supabase matches by Google sub. | Works (Google is cross-platform). | Works if same Google account. |
The takeaway: Game Center is good enough for single-Apple-ID recovery (which is the vast majority of cases), but Google is the most portable option. For a family where a kid plays on Mom's phone and later gets their own device with a different Apple ID, Google is the only automatic recovery path.
No. Apple deprecated UDID in 2013 and removed identifierForVendor persistence across reinstalls in later iOS versions. identifierForVendor resets when the user deletes all apps from your developer team and reinstalls. There is no universal, persistent device identifier on iOS that survives app deletion. This is by design (privacy). The only persistent identifiers are those tied to an account: Apple ID (via Game Center or Sign in with Apple), Google account, or an email/password you store yourself.
The strongest approach for your game specifically: all creatures are uploaded to Supabase Storage as soon as they're created, regardless of auth state. Anonymous users get cloud creature storage. The difference is whether they can recover access to those creatures from another device. Frame the sign-in prompt around this: "Your creatures are safe in the cloud, but only you can get them back. Link an account so you never lose them." This means even anonymous users' data is server-side, which makes manual recovery possible (support team looks up creatures by name/metadata) even if there's no automated path.
Don't prompt once and forget. Prompt when it matters:
| Trigger | Message framing |
|---|---|
| Created 3rd creature | "You've got 3 creatures now. Protect them." |
| Won 5th battle | "Nice streak. Don't risk losing your team." |
| 7 days of play, still anonymous | "You've been playing a week. One tap to save everything." |
| Before any IAP | Gate purchases behind linked accounts. This is also COPPA-smart. |
A 2GB download creates a unique funnel shape. The user has already committed significant time and bandwidth before they see your app. This changes the auth calculus:
Don't waste the investment. The user just waited for a large download. They are primed to play. Every second between "download complete" and "gameplay" is wasted goodwill. Auth must be invisible.
Consider on-demand resources. iOS supports On-Demand Resources (ODR) and Background Assets framework. If you can ship a ~200MB initial app with core gameplay, then stream the remaining ~1.8GB of AI models and assets in the background, you dramatically reduce the time-to-play. The user starts playing within a minute of tapping "Get" in the App Store while the full asset set downloads behind the scenes. This is orthogonal to auth but massively impacts the same funnel.
The auth/download sequencing: Auth should happen after the app is open and running, not during the download or install. On iOS, there is no mechanism to collect auth during download anyway—but some games attempt a "pre-register" flow via their website or social media. For your case, skip this entirely. The App Store > Download > App Launch > Anonymous Auth (instant) > Silent GC (instant) > Gameplay pipeline should take under 3 seconds from app launch to playable state.
Your "parents playing with kids, AI-themed game" audience likely qualifies as mixed-audience or child-directed under COPPA. The key restriction: you cannot collect personal information from users under 13 without Verifiable Parental Consent (VPC). "Personal information" includes email addresses, persistent identifiers, and (under the updated rule effective April 2026) device IDs and IP addresses.
The anonymous-first pattern is inherently COPPA-friendly. A locally-generated anonymous UUID with no PII transmission satisfies minimal data requirements. You don't collect email, name, or any identifier until the user opts into account linking—which you can gate behind a neutral age check.
Neutral age gate: Before any data collection beyond internal operations, ask for date of birth (not age directly, and never telegraph the "correct" answer). If the user is under 13, operate in zero-data mode. If 13+, proceed with standard auth flows. Place this gate before the "Save your progress?" prompt, not at app launch.
Game Center special case: Game Center's isUnderage flag is set by Apple based on the Apple ID's date of birth. If isUnderage == true, the player can still authenticate (Game Center handles parental consent at the system level), but you should restrict social features and data collection in your app.
signInAnonymously() on every app launch that doesn't have an existing session. This is the foundation everything else builds on. Ship the game with just this and you have a functional auth system.
linkIdentity() options in the post-tutorial "Save your progress?" prompt. Apple is mandatory once Google is present. Both use native signInWithIdToken()—straightforward Supabase integration.
This is the most important section for understanding what actually happens on the device and server. Where does the session live? What happens when you close the app? What happens when you delete the app?
When Supabase's signInAnonymously() succeeds, it creates two things: a server-side record in auth.users and auth.sessions (on Supabase's Postgres), and a client-side token pair (access token JWT + refresh token string). The client library stores this token pair in whatever storage backend you configure. In a browser, that's localStorage by default. In Godot, you're using the supabase-community/godot-engine.supabase addon, which stores the tokens however you implement the storage adapter. For a mobile game, you should write the tokens to a file in the app's sandboxed data directory (e.g., user://auth_session.json in Godot's user:// path).
| Event | What happens to session | User impact |
|---|---|---|
| App backgrounded / closed | Tokens remain in user:// storage. Nothing changes. |
None. Seamless resume. |
| App killed from task switcher | Tokens remain on disk. On next launch, getSession() reads them, auto-refreshes if the access token expired. |
None. Seamless resume. |
| Device rebooted | Same as above. Tokens persist on disk. | None. |
| App deleted / reinstalled | Tokens are destroyed. The user:// directory is wiped by iOS when the app is uninstalled. The server-side session still exists in Supabase but the client has no way to reach it. |
Anonymous account is orphaned forever. On reinstall, signInAnonymously() creates a brand new user. Old creatures, progress, everything is gone. |
| "Offload App" (iOS storage management) | Tokens are preserved. iOS offloading removes the app binary but keeps the Documents and user:// data. When the user reinstalls, the data is restored. |
None, if tokens are in user://. |
Supabase refresh tokens never expire on their own but can only be used once. When you use a refresh token to get a new access token, Supabase issues a new refresh token and invalidates the old one. The client library handles this automatically via an onAuthStateChange listener that fires a TOKEN_REFRESHED event. You must persist the new token pair every time this event fires, or you'll end up with a stale refresh token that's already been consumed.
Sessions can be configured to have a maximum lifetime or inactivity timeout (Pro plan+), but by default, sessions are indefinite. A user who launches the game once a month will still have a valid session as long as the refresh token is intact on the device. If they don't launch for a very long time and you've configured an inactivity timeout, the session terminates server-side and the client gets an error on the next refresh attempt. For a game, don't set an inactivity timeout on anonymous sessions.
_store_session() function is the single most important piece of auth code in the game. Every time onAuthStateChange fires with SIGNED_IN or TOKEN_REFRESHED, you must write the new access + refresh token to disk. If you miss a refresh cycle and the old token gets rotated out, the user's session is dead. For anonymous users, that means permanent account loss.
"Skip" means the user stays on their anonymous Supabase session with no linked identity provider. Their creatures and progress are stored server-side under their anonymous user_id, and the session tokens live in user:// on the device. They can play normally. The risk is: if they delete the app, lose their phone, or factory reset, their account is unrecoverable. They have no email, no Game Center ID, no Apple ID attached. There is no recovery path.
This is acceptable as a default state but you should make the risk clear to the user. The "Save your progress?" prompt should communicate: "Your creatures live on this device. If you lose this device or delete the app, they're gone forever. Sign in to protect them." Periodically re-prompt (e.g., when they create their 5th creature, or hit level 10, or after 3 days of play) if they're still anonymous.
Default 30 anonymous sign-ups per hour per IP. Enable CAPTCHA in Supabase dashboard. Use RLS policies like (auth.jwt()->>'is_anonymous')::boolean IS FALSE to gate premium features behind linked accounts.
When the user taps "Sign in with Apple" from the post-tutorial prompt, the anonymous user's UUID is preserved. Supabase's linkIdentity() attaches the Apple identity to the existing user record. All game data, progress, and creatures remain on the same user_id. The JWT's is_anonymous claim flips to false, and any RLS-gated features unlock immediately. No data migration, no UUID change, no server-side gymnastics.
const SESSION_PATH = "user://auth_session.json"
func _ready():
# Listen for ALL auth state changes
supabase.auth.on_auth_state_change.connect(_on_auth_change)
# Try to restore existing session from disk
var saved = _load_session_from_disk()
if saved:
await supabase.auth.set_session(saved.access_token, saved.refresh_token)
else:
# No saved session: first launch or reinstall
await supabase.auth.sign_in_anonymously()
func _on_auth_change(event, session):
if event in ["SIGNED_IN", "TOKEN_REFRESHED"]:
# ALWAYS persist new tokens to disk immediately
_save_session_to_disk(session)
func _save_session_to_disk(session):
var file = FileAccess.open(SESSION_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify({
"access_token": session.access_token,
"refresh_token": session.refresh_token
}))
func _load_session_from_disk():
if not FileAccess.file_exists(SESSION_PATH):
return null
var file = FileAccess.open(SESSION_PATH, FileAccess.READ)
return JSON.parse_string(file.get_as_text())
Game Center authentication is silent, signature-based, and not OAuth. It operates through GKLocalPlayer.local.authenticateHandler, a closure that iOS calls multiple times throughout the app lifecycle—on initial set, on auth state changes, and when the user switches accounts.
If the user is signed into Game Center via iOS Settings (the default for any device with an Apple ID), the handler fires with isAuthenticated = true and a "Welcome back" banner slides from the top. No UI is presented. No user interaction. This is why Game Center is Tier 1 in the cascade.
If the user is not signed in, a login UIViewController is returned exactly once. If the user dismisses it, the app cannot show it again until the user signs in via Settings manually. This is an iOS-level restriction—there is no workaround.
Game Center provides no JWT, no OAuth token, and no email address. Instead, it uses fetchItems(forIdentityVerificationSignature:) (iOS 13.5+), which returns five values:
| Value | Purpose |
|---|---|
publicKeyURL | URL to Apple's signing certificate at static.gc.apple.com |
signature | RSA-SHA256 signature over the verification payload |
salt | Random salt included in the signed payload |
timestamp | Unix timestamp (big-endian) when signature was generated |
teamPlayerID | Persistent player identifier scoped to your developer team |
Your server reconstructs the payload by concatenating teamPlayerID + bundleID + timestamp(bigEndian) + salt, fetches Apple's public certificate from publicKeyURL, and verifies the RSA-SHA256 signature. If it checks out, the user is who they say they are.
publicKeyURL starts with https://static.gc.apple.com/—an attacker could substitute their own certificate server. Also verify the certificate chain roots to DigiCert and reject signatures older than ~5 minutes to prevent replay attacks.
teamPlayerID is persistent across all your games under the same Apple Developer team. gamePlayerID is scoped to a single game. Use teamPlayerID as your canonical identifier—it lets you recognize the same player across Bobium Brawlers and any future titles. Also available: displayName, alias, isUnderage, and isMultiplayerGamingRestricted.
Add the Game Center capability in Signing & Capabilities. This sets the com.apple.developer.game-center entitlement and links GameKit.framework. No Info.plist modifications needed. Game Center must also be enabled in App Store Connect under your app's Services tab. That's it.
Game Center is not a built-in Supabase provider and can't be added through standard configuration (it's not OAuth/OIDC). The solution is a Supabase Edge Function that verifies Game Center signatures and creates/retrieves Supabase sessions.
The Edge Function creates Supabase users with a synthetic email like gc_{teamPlayerId}@gamecenter.internal since Game Center provides no real email. The teamPlayerID goes into app_metadata for lookup on subsequent logins. Use admin.createUser({ email, email_confirm: true }) to skip verification, then admin.generateLink({ type: 'magiclink' }) to produce a session.
The client receives a standard Supabase JWT and uses it for all subsequent API calls. RLS works normally via auth.uid().
When Game Center succeeds during Phase 1, the user already has an anonymous Supabase session. Two approaches:
Option A (simpler): The Edge Function receives the anonymous user's JWT alongside the Game Center payload. It verifies the GC signature, then calls admin.updateUserById() on the existing anonymous user to set app_metadata.gc_team_player_id and flip is_anonymous to false. Same UUID, no data migration.
Option B (more standard): The Edge Function creates or finds the GC-linked user, returns a new session, and the client replaces its session. Downside: the anonymous user's data must be migrated to the new user ID. Option A is strongly recommended.
Supabase supports Apple natively via signInWithIdToken(). The iOS flow uses Apple's AuthenticationServices framework: generate a nonce, request authorization via ASAuthorizationAppleIDProvider, pass the resulting identity token to Supabase. Apple provides an email (possibly a private relay address like abc@privaterelay.appleid.com) and the user's name only on first authorization—you must persist the name immediately.
Xcode requirement: Add the "Sign in with Apple" capability, which sets the com.apple.developer.applesignin entitlement.
GodotApplePlugins includes Sign in with Apple support, so the same plugin handles both Game Center and Apple Sign-In.
Google's iOS SDK presents a native bottom sheet where the user selects an account. Pass the resulting ID token to signInWithIdToken() with provider: 'google'. Configuration requires creating both a Web Application and iOS OAuth client in Google Cloud Console. The Web client ID and secret go into the Supabase dashboard; the iOS client ID enables native SDK auth.
Supabase provides linkIdentity() for OAuth providers and updateUser() for email. Enable "Manual Linking" in the Supabase dashboard under Auth > Providers. When an anonymous user links an identity, their UUID is preserved—no data migration. The is_anonymous JWT claim flips to false automatically.
| Provider | Supabase method | Link method | Godot plugin |
|---|---|---|---|
| Game Center | Custom Edge Function | admin.updateUserById() | GodotApplePlugins |
| Apple | signInWithIdToken() | linkIdentity() | GodotApplePlugins |
signInWithIdToken() | linkIdentity() | Google Sign-In iOS SDK | |
signUp() | updateUser({ email }) | HTTP (no native SDK) |
| Plugin | Architecture | GC Support | Status |
|---|---|---|---|
GodotApplePluginsmigueldeicaza/GodotApplePlugins |
SwiftGodot / GDExtension | Full (GameKit, StoreKit2, Sign in with Apple) | Active, Godot 4.2+, Asset Library |
godot-ios-pluginsgodot-sdk-integrations/godot-ios-plugins |
Legacy Obj-C++ (.gdip) | Game Center plugin exists | 31 open issues, 21 open PRs |
godot-ios-extensionsrktprof/godot-ios-extensions |
Swift / GDNative | GameKit, StoreKit, Sign in with Apple | Maintained, alternative option |
| supabase-community/godot-engine.supabase | Pure GDScript (REST/HTTP) | N/A (auth client only) | ~228 stars, Auth+DB+Realtime+Storage |
GodotApplePlugins is the clear first choice. It's built on SwiftGodot (the modern GDExtension Swift binding), maps 1:1 to Apple's GK*/AS* APIs using snake_case, and is maintained by Miguel de Icaza. Drop the binaries into addons/.
GDScript calls a plugin method → SwiftGodot bridges to Swift → Swift calls iOS API → iOS presents auth UI (if needed) → Swift receives callback → onComplete.callDeferred() returns data to GDScript.
iOS plugins return null in the Godot editor and on non-iOS platforms. Always guard with ClassDB.class_exists():
var _game_center: Variant = null
func _ready():
# Always start with anonymous Supabase auth
var session = await supabase.auth.sign_in_anonymously()
_store_session(session)
# Then attempt silent Game Center
if ClassDB.class_exists("GameCenter"):
_game_center = ClassDB.instantiate("GameCenter")
_game_center.authenticate(_on_gc_auth)
func _on_gc_auth(error, player):
if error == OK:
# Get signature data for server verification
var sig_data = await player.fetch_identity_verification_signature()
# POST to Edge Function with sig + current anon JWT
var result = await _call_edge_function(
"gamecenter-auth", {
"public_key_url": sig_data.public_key_url,
"signature": sig_data.signature,
"salt": sig_data.salt,
"timestamp": sig_data.timestamp,
"team_player_id": player.team_player_id,
"anon_user_id": supabase.auth.current_user.id
})
if result.ok:
_store_session(result.session)
else:
# Game Center unavailable. User stays anonymous.
# Will be prompted post-tutorial.
pass
else branch.
| Capability | Entitlement key | When needed | Plist changes |
|---|---|---|---|
| Game Center | com.apple.developer.game-center |
Always (Tier 1 auth) | None required |
| Sign in with Apple | com.apple.developer.applesignin |
If offering any 3rd-party sign-in | None required |
| Push Notifications | aps-environment |
If using push for re-engagement | None required |
| Associated Domains | com.apple.developer.associated-domains |
If using universal links for deep linking / magic link email auth | None required |
All capabilities are added in Xcode under Signing & Capabilities. Godot's iOS export template respects the .entitlements file you configure. GodotApplePlugins documentation covers the exact file paths.
For Google Sign-In: add the reversed Google client ID as a URL scheme in your Info.plist (format: com.googleusercontent.apps.YOUR_CLIENT_ID). No entitlement needed.