Patreon OAuth 2.0 Integration
Lantern OS includes a built-in Patreon OAuth login system that gates the entire site and maps Patreon tiers to role-based access levels.
Features
- OAuth 2.0 with PKCE — Secure authentication without storing passwords
- Session Management — Server-side session cookies with 7-day TTL
- Role-Based Access Control — Access tiers: guest, supporter, deep_dreamer (paid), admin (owner only)
- Profile Page — User can view their Patreon info and logout from
/profile.html - Auto-Redirect — Unauthenticated users redirected to
/auth.html - No Vendor Lock-in — Pure Node.js implementation, no external auth services
Setup
1. Create a Patreon OAuth App
- Go to https://www.patreon.com/portal/registration/register-oauth-application
- Fill in the OAuth app form:
- App Name: Lantern OS (or your custom name)
- Redirect URI:
http://127.0.0.1:4177/api/auth/patreon/callback(dev) orhttps://your-domain.com/api/auth/patreon/callback(production)
- Accept terms and submit
- Copy your Client ID and Client Secret — keep the secret safe!
2. Get Your Campaign ID (required)
PATREON_CAMPAIGN_ID is the numeric API campaign id, used to scope role gating to your campaign (a member's pledges to other creators must not count). It is not the vanity slug in the page URL — patreon.com/cw/UnisonaAI has no numeric id in it. Get it from the API while authenticated as the campaign owner:
curl -s https://www.patreon.com/api/oauth2/v2/campaigns \
-H "Authorization: Bearer <owner-access-token>" # → data[].id is your PATREON_CAMPAIGN_ID
If unset, gating only works for a user who backs exactly one campaign; anyone who also backs another creator fails closed to guest.
3. Configure .env
Add these variables to your .env file:
# Patreon OAuth credentials
PATREON_CLIENT_ID=your_client_id_here
PATREON_CLIENT_SECRET=your_client_secret_here
PATREON_REDIRECT_URI=http://127.0.0.1:4177/api/auth/patreon/callback
PATREON_CAMPAIGN_ID=your_campaign_id_here
# Session secret (can be any random string)
SESSION_SECRET=your_session_secret_here
For production, set:
PATREON_REDIRECT_URI=https://your-domain.com/api/auth/patreon/callbackNODE_ENV=production(enables secure cookies over HTTPS)
Local dev (.env.local, dual-boot ports)
For local development put the credentials in .env.local at repo root (gitignored — do not commit). The redirect_uri auto-derives from the request host, but Patreon requires an exact match, so register both dual-boot ports in the Patreon app's Redirect URIs:
http://localhost:4178/api/auth/patreon/callback (dev server)
http://localhost:4177/api/auth/patreon/callback (stable server)
Smoke test after restart:
# should redirect to patreon.com with your real client_id:
curl -s -i "http://localhost:4178/api/auth/patreon/start?returnTo=%2F" | grep -i location
Then click Connect with Patreon on /auth.html, complete consent, and confirm /api/auth/session shows provider:"patreon" and the mapped role.
4. Restart the Server
npm run dev --prefix apps/lantern-garage
The server will now:
- Redirect unauthenticated users to
/auth.html(Patreon login page) - Gate all pages behind OAuth
- Store sessions in memory (default) or connect a database for persistence
Role Mapping
Patreon tiers are mapped to roles by pledge amount (not campaign-specific tier IDs), so moving to a new campaign — e.g. patreon.com/cw/UnisonaAI — never breaks gating. The mapping is defined in apps/lantern-garage/lib/auth-providers.js (roleForAmountCents), and role resolution is scoped to your PATREON_CAMPAIGN_ID so a member's pledges to other creators can't grant (or block) a role here.
| Entitled pledge (this campaign) | Role | Plan / access |
|---|---|---|
| Free / not a paying member | guest |
Public pages only |
| ≥ $20 | deep_dreamer |
Pro — all paid features + real-money trading unlock |
| ≥ $200 | pilot |
Pilot — everything in Pro plus the autonomous AI trader |
Legacy note: the $5 Member tier is retired and no longer sold. Its
≥ $5 → supportermapping is retained only so grandfathered patrons keep their role; thatsupporterrole now sits at the Free plan floor (seeapps/lantern-garage/lib/plan-matrix.jsROLE_TO_PLAN). The currently sold ladder is Free / $20 Pro / $200 Pilot — do not re-advertise $5 as a paid gate.
Notes:
- A purchasable tier NEVER grants
admin.adminis full operator/staff access
(provider keys, GPU dispatch, feature flags, and the accounts console that can reset any password / grant admin), so tying it to a price point would let anyone buy site takeover. The top purchasable tier is pilot ($200) — it unlocks the autonomous AI trader but not admin; admin comes only from LANTERN_ADMIN_IDS.
- Fail-closed: a $0 free-tier member, a below-$5 custom pledge, or an entitlement whose
amount can't be resolved all yield guest (never a free paid role).
- Re-pricing? Override the thresholds (cents):
PATREON_SUPPORTER_CENTS(legacy),
PATREON_DEEP_DREAMER_CENTS, PATREON_PILOT_CENTS.
- Demotion: logging in via Patreon re-baselines the paid role to the current
entitlement, so a cancelled/downgraded membership loses the tier (staff roles set via setUserRole/override are never demoted by a login).
- Owner admin is account-bound: set
LANTERN_ADMIN_IDSto the campaign owner's Patreon
user id (numeric, from /api/oauth2/v2/identity while logged in as the owner). It changes when you move to a new Patreon account. If unset, no one is admin.
- Restart the server after changing any of these.
API Endpoints
All OAuth-related endpoints are in apps/lantern-garage/routes/auth.js:
GET /api/auth/session
Returns current session info.
Response (authenticated):
{
"authenticated": true,
"role": "supporter",
"user": {
"id": "12345",
"name": "John Doe",
"email": "[email protected]",
"tier": "<entitled-tier-id>"
}
}
Response (not authenticated):
{
"authenticated": false,
"role": "guest"
}
GET /api/auth/patreon/start
Initiates OAuth flow. Query parameter: returnTo (optional, defaults to /).
Redirects to: Patreon OAuth consent screen
GET /api/auth/patreon/callback
OAuth callback endpoint. Patreon redirects here with code and state parameters.
Redirects to: Original page or /dream-chat.html
POST /api/auth/logout
Clears session and logs out user.
Response:
{ "ok": true }
Files
| File | Purpose |
|---|---|
apps/lantern-garage/lib/oauth-core.js |
Provider-agnostic flow engine: handleOAuthStart / handleOAuthCallback (exchange → fetchUser → role → profile → session) |
apps/lantern-garage/lib/auth-providers.js |
Per-provider config incl. the patreon block (authorize/token URLs, scope, fetchUser, roleForAmountCents mapping) |
apps/lantern-garage/lib/user-profiles.js |
getOrCreateFromIdentity — profile creation (records patreonId + tier) |
apps/lantern-garage/lib/patreon-auth.js |
Legacy Patreon-specific OAuth logic (superseded by the provider-agnostic pair above) |
apps/lantern-garage/routes/auth.js |
Route handlers for OAuth endpoints |
apps/lantern-garage/public/auth.html |
Patreon login page with tier cards |
apps/lantern-garage/public/profile.html |
User profile page with logout button |
apps/lantern-garage/public/js/auth-gate.js |
Client-side auth enforcement script |
Troubleshooting
"User fetch failed: Bad Request"
The Patreon API rejected the identity endpoint. Check:
- Token is valid (token exchange succeeded but user fetch failed)
- Scope is correct:
identity identity.memberships - Bearer token is properly formatted in Authorization header
Session not persisting
Sessions use in-memory storage by default. Restart clears all sessions. For production:
- Connect a database store (e.g.,
connect-mongoorconnect-pg-simple) - Add to
server.js:
const store = require('connect-mongo')(session);
const sessionMiddleware = session({
store: new store({ url: 'mongodb://...' }),
// ... rest of config
});
Cookies not being set
Check:
httpOnly: trueis set (cookies are not accessible to JavaScript)- Domain/path match correctly (local dev uses
127.0.0.1:4177) - Browser allows third-party cookies for local development
OAuth redirect loop
Check PATREON_REDIRECT_URI matches:
- The exact URL in your OAuth app settings (case-sensitive)
- The URL in
handlePatreonStart()callback
Development vs. Production
| Setting | Dev | Prod |
|---|---|---|
PATREON_REDIRECT_URI |
http://127.0.0.1:4177/api/auth/patreon/callback |
https://your-domain.com/api/auth/patreon/callback |
NODE_ENV |
(not set) | production |
Cookies: secure |
false |
true (HTTPS only) |
| Session Store | Memory (cleared on restart) | Persistent database |
| Session Secret | Can be any string | Use openssl rand -hex 32 |
Security Considerations
- Client Secret — Never expose to the browser. Keep in
.envonly. - PKCE — OAuth flow uses PKCE (Proof Key for Code Exchange) for additional security.
- HTTPS — Set
secure: truein cookie config when deploying to production. - Same-Site — Cookies use
sameSite: 'lax'to prevent CSRF attacks. - HTTP-Only — Session cookies are
httpOnlyand cannot be accessed by JavaScript.
Testing
Test OAuth Flow in Browser
- Open
http://127.0.0.1:4177 - Click "Continue with Patreon" button
- Approve OAuth scope on Patreon
- Check
/profile.htmlto see your user info and role - Test logout button
Test Session Persistence
# After logging in, in browser console:
fetch('/api/auth/session', { credentials: 'include' })
.then(r => r.json())
.then(console.log)
Should return your user info and role.
Support
For issues or questions:
- Check the server logs for
[AUTH]debug messages - Review
.envconfiguration (PATREON_CAMPAIGN_ID + LANTERN_ADMIN_IDS) - Verify Patreon app settings match your redirect URI
- Open an issue on GitHub with full error logs