Outseta handles authentication and billing, but what about data that lives outside Outseta? User-generated content in your database, premium courses on your server, or API routes that need protection.

This guide explains when and why you need server-side protection.

Do you need server-side protection?

Not every Outseta integration requires this. Here's when you do and don't:

Scenario Need server-side protection?
Showing/hiding UI based on login status No — use Outseta.js client-side
Gating content behind a paywall (soft) No — use protected content
Storing user preferences or simple attributes No — use custom properties
Gating content behind a paywall (hard) Yes
Storing user-generated data Yes
Protecting serverless functions or API routes Yes

Soft vs. hard gating: Soft gating hides content client-side — quick to set up and works for most membership sites. Hard gating verifies the user on the server before sending content — use this when content is sensitive and must never reach unauthorized users.

Two scenarios

Imagine you're building a membership site for an online educator. There's a blog with articles, a library of courses, and members can leave comments. Outseta handles authentication and billing, but you need to build the content delivery and commenting system yourself.

Reading: Gating premium courses

Some courses are available to all members. Others — the advanced masterclasses — require a Pro plan or a one-time add-on purchase.

When a member clicks "Start Course," your server needs to answer: Does this person have access? You can't just check if they're logged in — you need to know their plan and what they've purchased.

GET /api/courses/:courseId

Writing: Member comments

Members can comment on articles and course lessons. When someone posts a comment, your server needs to answer: Who is this person? You can't trust a user ID sent from the browser — anyone could fake that.

POST /api/comments

Both scenarios have the same underlying question: How does your server know it can trust the request?

The solution: Verify the JWT Access Token

When a user logs in via Outseta, they receive a JWT (JSON Web Token) Access Token.

Outseta stores this token in localStorage, sessionStorage, or a cookie, depending on your configuration — retrieve it client-side with Outseta.getAccessToken() and send it with the request, or if using cookie storage, access it as Outseta.nocode.accessToken on the server.

Your backend verifies the signature to confirm that the token is authentic and unmodified. Only then can you trust its contents, including the user's identity and subscription details.

Key concept: Decode ≠ Verify

Anyone can decode a JWT and see its contents — try it yourself at jwt.io. But decoding doesn't prove the token is real.

Only verification proves that Outseta actually issued the token and it hasn't been tampered with. Never trust a decoded token without verification.

The flow

Let's go into more detail about how this would look for posting comments.

Sequence diagram showing the authentication flow: Browser logs in via Outseta, receives JWT, sends request to your API with token, API verifies with Outseta, then processes request

  1. User logs in — via Outseta's login embed on your site
  2. Receives JWT — Outseta returns a signed access token to the browser
  3. Browser makes request — sends the token in the Authorization header
  4. API verifies token — checks the signature against Outseta's public keys
  5. Token valid — extract user info from the verified payload
  6. Process request — return content or save data with the verified user ID

What's in the verified token?

Once verified, you can use these claims from the token payload:

  • sub — the user's unique Person ID (use this as your user identifier)
  • email — the user's email address
  • name — the user's full name
  • outseta:accountUid — the account they belong to
  • outseta:planUid — their current subscription plan
  • outseta:addOnUids — array of purchased add-ons

👉 Full payload reference: The JWT Access Token

Putting it together

Example: Reading protected content (cookie method)

// GET /api/courses/:courseId

1. Get the token from the cookie
token = request.cookies["Outseta.nocode.accessToken"]

2. Verify the token
payload = verifyToken(token)

3. Check if user has access
planUid = payload["outseta:planUid"]
addOnUids = payload["outseta:addOnUids"] || []

course = getCourse(request.params.courseId)

if (planUid in course.allowedPlans || course.addOnUid in addOnUids) {
return course.content
} else {
return 403 Forbidden
}

Example: Writing user-generated content (header method)

Client-side, get the token and send it in the Authorization header:

// Browser
const token = Outseta.getAccessToken()

fetch('/api/comments', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ articleId: '123', text: 'Great article!' })
})

Server-side, extract and verify:

// POST /api/comments

1. Get the token from the Authorization header
token = request.headers["Authorization"].replace("Bearer ", "")

2. Verify the token
payload = verifyToken(token)

3. Extract the user ID from the verified payload
userId = payload.sub

4. Save the comment with the verified user ID
INSERT INTO comments (user_id, article_id, text)
VALUES (userId, request.body.articleId, request.body.text)

5. Return success

The key insight: you never trust user-provided identity. You extract the user ID from the verified token — this guarantees it's authentic.

How to get a token for testing

Log into your Outseta-powered site, open your browser's developer console, and run:

Outseta.getAccessToken()

This returns the JWT for the currently logged-in user. Decode it at jwt.io to see the payload — but remember that your API must verify it before trusting its contents.

Next steps

👉 Verify Outseta JWT Access Tokens server-side — Implementation guide with code examples
👉 The JWT Access Token — Full payload reference
👉 Outseta's Well-Known Endpoints (JWK / OIDC) — Technical reference for JWKS
👉 Supabase + Outseta Auth with RLS — Database-specific implementation