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/portalClients & 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.