Skip to main content

Patreon Access

Standalone Patreon membership and tier checking for Unreal Engine, via in-game loopback OAuth.

  • No backend required. The token exchange runs client-side against Patreon's public OAuth endpoints.
  • No client secret ships. Patreon's token exchange works with client_id alone; the secret field exists only as a documented-optional hedge and should stay blank in shipped builds.
  • Project-agnostic. A single Runtime module with no game-side dependencies.
Supported platforms

Win64, Mac, Linux. The loopback flow requires a system browser and the ability to bind a localhost port, so the plugin is desktop-only by design.

How it works​

UPatreonAccessSubsystem::BeginLogin() opens the player's system browser on Patreon's consent page with a 127.0.0.1 redirect. A temporary local HTTP listener catches the redirect, exchanges the auth code for tokens, fetches the member's identity (entitled tiers with cents amounts), broadcasts the result, and caches it in a plugin-owned save slot for offline grace.

sequenceDiagram
participant Game
participant Browser as System browser
participant Patreon
Game->>Game: BeginLogin() — start loopback listener on 127.0.0.1:6167
Game->>Browser: Open consent URL (client_id, scope, state nonce)
Browser->>Patreon: Player approves access
Patreon->>Game: Redirect to http://127.0.0.1:6167/auth?code=...&state=...
Game->>Game: Validate state nonce (RFC 8252 CSRF guard)
Game->>Patreon: Exchange auth code for tokens (client_id only)
Game->>Patreon: Fetch identity (entitled tiers, cents amounts)
Game->>Game: Cache membership, broadcast OnMembershipUpdated

The flow is OAuth 2.0 authorization-code with a loopback redirect (the RFC 8252 native-app pattern), including the RFC's CSRF safeguard: each login carries a single-use random state nonce that the callback must echo. Forged or spurious localhost requests can neither complete nor cancel a pending login.

Token storage

Tokens are the player's own and are stored locally on-device only — there is no server-side storage. Token values are never written to logs.

Quick start​

  1. Register an OAuth client at patreon.com/portal → Clients & API Keys.
    • Redirect URI: http://127.0.0.1:6167/auth — must match LoopbackPort + CallbackPath if you change them.
  2. Set the Client ID in the editor: Project Settings → Game → Patreon Access → OAuth → Client Id. Leave Client Secret blank.
  3. Log in: call BeginLogin() (Blueprint or C++) from your UI, or use the Patreon.Login console command in a non-Shipping build.
  4. Gate content with the pure query functions.
Recommended gate

Use HasEntitledAmountAtLeast(Cents) rather than tier-title matching. Cents thresholds survive tier renames on the Patreon page; titles don't.

C++ example​

#include "PatreonAccessSubsystem.h"

void UMyMenuWidget::OnPatreonLoginClicked()
{
UPatreonAccessSubsystem* Patreon =
GetWorld()->GetGameInstance()->GetSubsystem<UPatreonAccessSubsystem>();

Patreon->OnMembershipUpdated.AddDynamic(this, &UMyMenuWidget::HandleMembershipUpdated);
Patreon->OnMembershipFailed.AddDynamic(this, &UMyMenuWidget::HandleMembershipFailed);
Patreon->BeginLogin();
}

void UMyMenuWidget::HandleMembershipUpdated(const FPatreonMembership& Membership)
{
// e.g. gate a $15 tier
const bool bPremium = Membership.HighestEntitledAmountCents >= 1500;
RefreshLockedContent(bPremium);
}

Blueprint example​

Get the Patreon Access Subsystem from the Game Instance, bind On Membership Updated / On Membership Failed, then call Begin Login. Gate content anywhere with the pure nodes (Has Entitled Amount At Least, Is Active Patron, etc.).

API reference​

All API lives on UPatreonAccessSubsystem, a UGameInstanceSubsystem — resolve it via the Game Instance from either Blueprint or C++.

Actions​

FunctionBehavior
BeginLogin()Starts the loopback listener and opens the browser consent page. Results arrive via the delegates. 120 s timeout.
RefreshMembership()Re-fetches membership with the stored token; one silent refresh-token retry on 401. Fails via delegate if not logged in.
Logout()Clears tokens, cached membership, and the cache file; broadcasts an empty membership so listeners drop granted state.

Delegates (BlueprintAssignable)​

DelegateSignatureFires
OnMembershipUpdated(const FPatreonMembership& Membership)After any successful login/refresh, and at boot from cache/auto-refresh.
OnMembershipFailed(EPatreonLoginResult Result, bool bAuthExpired)On failure. bAuthExpired = true means stored tokens no longer work — prompt a re-login.

Queries (BlueprintPure)​

FunctionReturns
GetMembership(bool& bValid)Cached FPatreonMembership; bValid = fetched within CacheLifetimeDays.
IsActivePatron()true when patron status is ActivePatron.
GetHighestEntitledAmountCents()Highest entitled tier amount in cents.
HasEntitledAmountAtLeast(int32 Cents)Recommended gate. true when the member's highest entitled tier meets the threshold.
HasTierTitled(const FString& Title)Convenience title match — prefer the cents check.
IsLoggedIn()true when an access token is stored.
IsRequestInFlight()true while a login/refresh request is running.

Types​

FPatreonMembership​

FieldTypeMeaning
UserIdFStringPatreon user id.
FullNameFStringMember display name.
PatronStatusEPatreonPatronStatusSee enum below.
EntitledTiersTArray<FPatreonTier>All currently entitled tiers.
HighestEntitledAmountCentsint32Highest AmountCents among entitled tiers — the recommended gate value.
FetchedAtUtcFDateTimeWhen this membership was fetched (drives the offline-grace window).

FPatreonTier​

FieldTypeMeaning
TierIdFStringStable Patreon tier id — use this for entitlement mappings.
TitleFStringDisplay title (can be renamed by the creator at any time).
AmountCentsint32Tier price in cents.

EPatreonPatronStatus​

Unknown · NoPatron · ActivePatron · DeclinedPatron · FormerPatron

EPatreonLoginResult​

Success · Cancelled · ListenerFailed · TokenExchangeFailed · IdentityFetchFailed

Settings​

Project Settings → Game → Patreon Access. Values persist to DefaultGame.ini.

PropertyDefaultNotes
ClientId(empty)From your Patreon client registration. Public by nature.
ClientSecret(empty)Optional; leave blank in shipped builds.
LoopbackPort6167Must match the registered redirect URI. Range 1024–65535.
CallbackPath/authMust match the registered redirect URI.
Scopeidentity identity.membershipsOAuth scopes requested.
AuthorizeURLPatreon's endpointAdvanced override.
TokenURLPatreon's endpointAdvanced override — point at your own proxy if Patreon ever requires a confidential client, with zero code changes.
IdentityURLPatreon's endpointAdvanced override.
UserAgentPatreonAccess/1.0Sent on all requests.
CacheLifetimeDays40Offline grace window — outlives a monthly billing cycle so patrons aren't nagged; lapsed members age out naturally.
bAutoRefreshOnBoottrueSilently re-fetch membership at startup when tokens exist.
Keep the client secret out of shipped builds

Patreon's token exchange is documented to work with client_id alone. ClientSecret exists only as a hedge against a future policy change — anything you put there ships in plaintext config, so leave it blank and use a TokenURL proxy instead if a confidential client ever becomes mandatory.

Console commands​

Compiled out of Shipping builds.

CommandEffect
Patreon.LoginRuns the full browser login flow.
Patreon.RefreshRe-fetches membership with stored tokens.
Patreon.StatusLogs the cached membership — status, tiers, amounts. Useful for grabbing real tier IDs while wiring up an entitlement mapping.
Patreon.LogoutClears tokens and cache.

Caching and offline behavior​

  • Membership is cached in the plugin-owned save slot "PatreonAccess", separate from any game save.
  • A cached membership keeps granting for CacheLifetimeDays (default 40) without a network connection, so patrons aren't re-prompted every launch. Lapsed members age out naturally when the cache expires.
  • With bAutoRefreshOnBoot enabled, the subsystem silently re-fetches membership at startup whenever tokens exist and broadcasts OnMembershipUpdated on success.

Troubleshooting​

SymptomLikely cause
Browser opens but the game never receives the callbackRedirect URI on the Patreon client doesn't exactly match http://127.0.0.1:<LoopbackPort><CallbackPath>, or another process holds the port (ListenerFailed).
OnMembershipFailed with bAuthExpired = trueStored tokens were revoked or expired past refresh — prompt the player to log in again.
Login silently ignored requests to the callback URLThe state nonce didn't match — by design, mismatched callbacks neither complete nor cancel a pending login. Re-run BeginLogin().
Nothing happens after 120 sLogin timeout — the player closed the browser or never completed consent. OnMembershipFailed fires with Cancelled.

Copyright (c) 2026 Krypsis Syndicate LLC. All Rights Reserved.