From 0b873eafcca681f6da2815236e43ed45becdea27 Mon Sep 17 00:00:00 2001
From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com>
Date: Tue, 27 Jan 2026 01:33:37 +0000
Subject: [PATCH 1/5] Document isRestricted and restrictedReason JWT claims
---
docs/code-examples/concepts.ts | 140 ++++++++++++++++++++
docs/code-examples/index.ts | 2 +
docs/content/docs/(guides)/concepts/jwt.mdx | 101 +++++---------
3 files changed, 177 insertions(+), 66 deletions(-)
create mode 100644 docs/code-examples/concepts.ts
diff --git a/docs/code-examples/concepts.ts b/docs/code-examples/concepts.ts
new file mode 100644
index 0000000000..d3b253cd02
--- /dev/null
+++ b/docs/code-examples/concepts.ts
@@ -0,0 +1,140 @@
+import { CodeExample } from '../lib/code-examples';
+
+export const conceptsExamples = {
+ 'jwt': {
+ 'client-side-usage': [
+ {
+ language: 'JavaScript',
+ framework: 'Next.js',
+ code: `import { useUser } from '@stackframe/stack';
+
+export function UserProfile() {
+ const user = useUser();
+
+ if (!user) {
+ return
Please sign in
;
+ }
+
+ return Welcome, {user.displayName}!
;
+}`,
+ highlightLanguage: 'tsx',
+ filename: 'app/components/user-profile.tsx'
+ },
+ {
+ language: 'JavaScript',
+ framework: 'React',
+ code: `import { useUser } from '@stackframe/react';
+
+export function UserProfile() {
+ const user = useUser();
+
+ if (!user) {
+ return Please sign in
;
+ }
+
+ return Welcome, {user.displayName}!
;
+}`,
+ highlightLanguage: 'tsx',
+ filename: 'components/UserProfile.tsx'
+ },
+ ] as CodeExample[],
+
+ 'server-side-usage': [
+ {
+ language: 'JavaScript',
+ framework: 'Next.js',
+ code: `import { stackServerApp } from '@/stack';
+
+export async function GET() {
+ const user = await stackServerApp.getUser();
+
+ if (!user) {
+ return new Response('Unauthorized', { status: 401 });
+ }
+
+ // Access user information from the JWT
+ return Response.json({
+ id: user.id,
+ displayName: user.displayName,
+ primaryEmail: user.primaryEmail,
+ selectedTeamId: user.selectedTeamId,
+ // Other user properties...
+ });
+}`,
+ highlightLanguage: 'typescript',
+ filename: 'app/api/user/route.ts'
+ },
+ ] as CodeExample[],
+
+ 'manual-jwt-verification': [
+ {
+ language: 'JavaScript',
+ framework: 'Node.js',
+ code: `import * as jose from 'jose';
+
+// Get the public key set from Stack Auth
+const jwks = jose.createRemoteJWKSet(
+ new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json')
+);
+
+// Verify a regular (non-anonymous) access token
+try {
+ const { payload } = await jose.jwtVerify(token, jwks, {
+ issuer: 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID',
+ audience: 'YOUR_PROJECT_ID',
+ });
+
+ console.log('JWT is valid:', payload);
+} catch (error) {
+ console.error('JWT verification failed:', error);
+}`,
+ highlightLanguage: 'typescript',
+ filename: 'verify-jwt.ts'
+ },
+ ] as CodeExample[],
+
+ 'manual-jwt-verification-anonymous': [
+ {
+ language: 'JavaScript',
+ framework: 'Node.js',
+ code: `import * as jose from 'jose';
+
+const jwks = jose.createRemoteJWKSet(
+ new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true')
+);
+
+const { payload } = await jose.jwtVerify(token, jwks, {
+ issuer: [
+ 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID',
+ 'https://api.stack-auth.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID',
+ ],
+ audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon'],
+});`,
+ highlightLanguage: 'typescript',
+ filename: 'verify-jwt.ts'
+ },
+ ] as CodeExample[],
+
+ 'manual-jwt-verification-restricted': [
+ {
+ language: 'JavaScript',
+ framework: 'Node.js',
+ code: `import * as jose from 'jose';
+
+const jwks = jose.createRemoteJWKSet(
+ new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true&include_restricted=true')
+);
+
+const { payload } = await jose.jwtVerify(token, jwks, {
+ issuer: [
+ 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID',
+ 'https://api.stack-auth.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID',
+ ],
+ audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon', 'YOUR_PROJECT_ID:restricted'],
+});`,
+ highlightLanguage: 'typescript',
+ filename: 'verify-jwt.ts'
+ },
+ ] as CodeExample[],
+ },
+};
diff --git a/docs/code-examples/index.ts b/docs/code-examples/index.ts
index 7d690775d1..2aaa8f4bfd 100644
--- a/docs/code-examples/index.ts
+++ b/docs/code-examples/index.ts
@@ -1,5 +1,6 @@
import { CodeExample } from '../lib/code-examples';
import { apiKeysExamples } from './api-keys';
+import { conceptsExamples } from './concepts';
import { customizationExamples } from './customization';
import { paymentsExamples } from './payments';
import { selfHostExamples } from './self-host';
@@ -9,6 +10,7 @@ import { viteExamples } from './vite-example';
const allExamples: Record>> = {
'setup': setupExamples,
'apps': {...apiKeysExamples, ...paymentsExamples },
+ 'concepts': conceptsExamples,
'getting-started': viteExamples,
'others': selfHostExamples,
'customization': customizationExamples,
diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx
index bb4b607316..135b527714 100644
--- a/docs/content/docs/(guides)/concepts/jwt.mdx
+++ b/docs/content/docs/(guides)/concepts/jwt.mdx
@@ -48,6 +48,8 @@ Stack Auth JWTs contain standardized headers and claims that power authenticatio
- **`email_verified`**: Whether the user's email has been verified
- **`selected_team_id`**: The currently selected team ID (nullable)
- **`is_anonymous`**: Whether this is an anonymous user session
+- **`is_restricted`**: Whether the user is restricted (e.g., unverified email or anonymous)
+- **`restricted_reason`**: Why the user is restricted (nullable). The `type` field is either `anonymous` or `email_not_verified`
## Example JWT Payload
@@ -68,7 +70,9 @@ Here's what a typical Stack Auth JWT payload looks like:
"email": "john@example.com",
"email_verified": true,
"selected_team_id": "team_789",
- "is_anonymous": false
+ "is_anonymous": false,
+ "is_restricted": false,
+ "restricted_reason": null
}
```
@@ -77,6 +81,14 @@ Anonymous user tokens have the same shape, but:
- `iss` becomes `https://api.stack-auth.com/api/v1/projects-anonymous-users/`
- `aud` becomes `:anon`
- `is_anonymous` is `true`
+- `is_restricted` is `true`
+- `restricted_reason` is `{ "type": "anonymous" }`
+
+Restricted user tokens (e.g., users who haven't verified their email when verification is required) have:
+
+- `aud` becomes `:restricted`
+- `is_restricted` is `true`
+- `restricted_reason` is `{ "type": "email_not_verified" }`
## Working with JWTs
@@ -84,85 +96,42 @@ Anonymous user tokens have the same shape, but:
Stack Auth automatically handles JWT tokens for you. When you use hooks like `useUser()`, the JWT is automatically included in API requests:
-```tsx
-import { useUser } from '@stackframe/stack';
-
-export function UserProfile() {
- const user = useUser();
-
- if (!user) {
- return Please sign in
;
- }
-
- return Welcome, {user.displayName}!
;
-}
-```
+
### Server-Side Usage
On the server side, you can access the JWT and its claims through the Stack Auth API:
-```tsx
-import { stackServerApp } from '@/stack';
-
-export async function GET() {
- const user = await stackServerApp.getUser();
-
- if (!user) {
- return new Response('Unauthorized', { status: 401 });
- }
-
- // Access user information from the JWT
- return Response.json({
- id: user.id,
- displayName: user.displayName,
- primaryEmail: user.primaryEmail,
- selectedTeamId: user.selectedTeamId,
- // Other user properties...
- });
-}
-```
+
### Manual JWT Verification
If you need to manually verify a JWT (for example, in a different service), fetch the public keys from Stack Auth's JWKS endpoint. Keys are derived per audience so the `kid` in the JWT header always matches one of the published keys.
-```typescript
-import * as jose from 'jose';
+
-// Get the public key set from Stack Auth
-const jwks = jose.createRemoteJWKSet(
- new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json')
-);
-
-// Verify a regular (non-anonymous) access token
-try {
- const { payload } = await jose.jwtVerify(token, jwks, {
- issuer: 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID',
- audience: 'YOUR_PROJECT_ID',
- });
+To support anonymous sessions, include those keys and allow both issuers and audiences:
- console.log('JWT is valid:', payload);
-} catch (error) {
- console.error('JWT verification failed:', error);
-}
-```
+
-To support anonymous sessions, include those keys and allow both issuers and audiences:
+To support restricted users (e.g., users who haven't verified their email), add `include_restricted=true`:
-```typescript
-const jwks = jose.createRemoteJWKSet(
- new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true')
-);
-
-const { payload } = await jose.jwtVerify(token, jwks, {
- issuer: [
- 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID',
- 'https://api.stack-auth.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID',
- ],
- audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon'],
-});
-```
+
### Signing Keys
From b9eb62db881b4d898ae241fa18e3c4f958ba26bf Mon Sep 17 00:00:00 2001
From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com>
Date: Thu, 29 Jan 2026 22:48:40 +0000
Subject: [PATCH 2/5] Sync documentation updates
---
docs/code-examples/concepts/index.ts | 5 +
.../{concepts.ts => concepts/jwt.ts} | 110 +++++++++---------
2 files changed, 59 insertions(+), 56 deletions(-)
create mode 100644 docs/code-examples/concepts/index.ts
rename docs/code-examples/{concepts.ts => concepts/jwt.ts} (57%)
diff --git a/docs/code-examples/concepts/index.ts b/docs/code-examples/concepts/index.ts
new file mode 100644
index 0000000000..117b4cb054
--- /dev/null
+++ b/docs/code-examples/concepts/index.ts
@@ -0,0 +1,5 @@
+import { jwtExamples } from './jwt';
+
+export const conceptsExamples = {
+ 'jwt': jwtExamples,
+};
diff --git a/docs/code-examples/concepts.ts b/docs/code-examples/concepts/jwt.ts
similarity index 57%
rename from docs/code-examples/concepts.ts
rename to docs/code-examples/concepts/jwt.ts
index d3b253cd02..155ab40bbe 100644
--- a/docs/code-examples/concepts.ts
+++ b/docs/code-examples/concepts/jwt.ts
@@ -1,12 +1,11 @@
-import { CodeExample } from '../lib/code-examples';
+import { CodeExample } from '../../lib/code-examples';
-export const conceptsExamples = {
- 'jwt': {
- 'client-side-usage': [
- {
- language: 'JavaScript',
- framework: 'Next.js',
- code: `import { useUser } from '@stackframe/stack';
+export const jwtExamples = {
+ 'client-side-usage': [
+ {
+ language: 'JavaScript',
+ framework: 'Next.js',
+ code: `import { useUser } from '@stackframe/stack';
export function UserProfile() {
const user = useUser();
@@ -17,13 +16,13 @@ export function UserProfile() {
return Welcome, {user.displayName}!
;
}`,
- highlightLanguage: 'tsx',
- filename: 'app/components/user-profile.tsx'
- },
- {
- language: 'JavaScript',
- framework: 'React',
- code: `import { useUser } from '@stackframe/react';
+ highlightLanguage: 'tsx',
+ filename: 'app/components/user-profile.tsx'
+ },
+ {
+ language: 'JavaScript',
+ framework: 'React',
+ code: `import { useUser } from '@stackframe/react';
export function UserProfile() {
const user = useUser();
@@ -34,16 +33,16 @@ export function UserProfile() {
return Welcome, {user.displayName}!
;
}`,
- highlightLanguage: 'tsx',
- filename: 'components/UserProfile.tsx'
- },
- ] as CodeExample[],
+ highlightLanguage: 'tsx',
+ filename: 'components/UserProfile.tsx'
+ },
+ ] as CodeExample[],
- 'server-side-usage': [
- {
- language: 'JavaScript',
- framework: 'Next.js',
- code: `import { stackServerApp } from '@/stack';
+ 'server-side-usage': [
+ {
+ language: 'JavaScript',
+ framework: 'Next.js',
+ code: `import { stackServerApp } from '@/stack';
export async function GET() {
const user = await stackServerApp.getUser();
@@ -61,16 +60,16 @@ export async function GET() {
// Other user properties...
});
}`,
- highlightLanguage: 'typescript',
- filename: 'app/api/user/route.ts'
- },
- ] as CodeExample[],
+ highlightLanguage: 'typescript',
+ filename: 'app/api/user/route.ts'
+ },
+ ] as CodeExample[],
- 'manual-jwt-verification': [
- {
- language: 'JavaScript',
- framework: 'Node.js',
- code: `import * as jose from 'jose';
+ 'manual-jwt-verification': [
+ {
+ language: 'JavaScript',
+ framework: 'Node.js',
+ code: `import * as jose from 'jose';
// Get the public key set from Stack Auth
const jwks = jose.createRemoteJWKSet(
@@ -88,16 +87,16 @@ try {
} catch (error) {
console.error('JWT verification failed:', error);
}`,
- highlightLanguage: 'typescript',
- filename: 'verify-jwt.ts'
- },
- ] as CodeExample[],
+ highlightLanguage: 'typescript',
+ filename: 'verify-jwt.ts'
+ },
+ ] as CodeExample[],
- 'manual-jwt-verification-anonymous': [
- {
- language: 'JavaScript',
- framework: 'Node.js',
- code: `import * as jose from 'jose';
+ 'manual-jwt-verification-anonymous': [
+ {
+ language: 'JavaScript',
+ framework: 'Node.js',
+ code: `import * as jose from 'jose';
const jwks = jose.createRemoteJWKSet(
new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true')
@@ -110,16 +109,16 @@ const { payload } = await jose.jwtVerify(token, jwks, {
],
audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon'],
});`,
- highlightLanguage: 'typescript',
- filename: 'verify-jwt.ts'
- },
- ] as CodeExample[],
+ highlightLanguage: 'typescript',
+ filename: 'verify-jwt.ts'
+ },
+ ] as CodeExample[],
- 'manual-jwt-verification-restricted': [
- {
- language: 'JavaScript',
- framework: 'Node.js',
- code: `import * as jose from 'jose';
+ 'manual-jwt-verification-restricted': [
+ {
+ language: 'JavaScript',
+ framework: 'Node.js',
+ code: `import * as jose from 'jose';
const jwks = jose.createRemoteJWKSet(
new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true&include_restricted=true')
@@ -132,9 +131,8 @@ const { payload } = await jose.jwtVerify(token, jwks, {
],
audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon', 'YOUR_PROJECT_ID:restricted'],
});`,
- highlightLanguage: 'typescript',
- filename: 'verify-jwt.ts'
- },
- ] as CodeExample[],
- },
+ highlightLanguage: 'typescript',
+ filename: 'verify-jwt.ts'
+ },
+ ] as CodeExample[],
};
From 6c2dc5a6d7c9fbb569e8c54d64ec6fc7132d11bb Mon Sep 17 00:00:00 2001
From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com>
Date: Thu, 29 Jan 2026 23:07:05 +0000
Subject: [PATCH 3/5] Sync documentation updates
---
docs/code-examples/concepts/jwt.ts | 2 ++
docs/content/docs/(guides)/concepts/jwt.mdx | 1 +
2 files changed, 3 insertions(+)
diff --git a/docs/code-examples/concepts/jwt.ts b/docs/code-examples/concepts/jwt.ts
index 155ab40bbe..64fd005bdf 100644
--- a/docs/code-examples/concepts/jwt.ts
+++ b/docs/code-examples/concepts/jwt.ts
@@ -124,6 +124,8 @@ const jwks = jose.createRemoteJWKSet(
new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true&include_restricted=true')
);
+// Restricted (non-anonymous) users use the same issuer as regular users,
+// so only two issuers are needed even though there are three audiences
const { payload } = await jose.jwtVerify(token, jwks, {
issuer: [
'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID',
diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx
index 135b527714..92075c83b9 100644
--- a/docs/content/docs/(guides)/concepts/jwt.mdx
+++ b/docs/content/docs/(guides)/concepts/jwt.mdx
@@ -86,6 +86,7 @@ Anonymous user tokens have the same shape, but:
Restricted user tokens (e.g., users who haven't verified their email when verification is required) have:
+- `iss` remains unchanged (same as regular users)
- `aud` becomes `:restricted`
- `is_restricted` is `true`
- `restricted_reason` is `{ "type": "email_not_verified" }`
From 7f02cacf86ef71707042e5d630a8d1c945e5a6f7 Mon Sep 17 00:00:00 2001
From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com>
Date: Thu, 29 Jan 2026 23:27:06 +0000
Subject: [PATCH 4/5] Sync documentation updates
---
docs/code-examples/concepts/jwt.ts | 4 ++--
docs/content/docs/(guides)/concepts/jwt.mdx | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/code-examples/concepts/jwt.ts b/docs/code-examples/concepts/jwt.ts
index 64fd005bdf..ec6baca1d4 100644
--- a/docs/code-examples/concepts/jwt.ts
+++ b/docs/code-examples/concepts/jwt.ts
@@ -124,12 +124,12 @@ const jwks = jose.createRemoteJWKSet(
new URL('https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID/.well-known/jwks.json?include_anonymous=true&include_restricted=true')
);
-// Restricted (non-anonymous) users use the same issuer as regular users,
-// so only two issuers are needed even though there are three audiences
+// All three user types have different issuers
const { payload } = await jose.jwtVerify(token, jwks, {
issuer: [
'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID',
'https://api.stack-auth.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID',
+ 'https://api.stack-auth.com/api/v1/projects-restricted-users/YOUR_PROJECT_ID',
],
audience: ['YOUR_PROJECT_ID', 'YOUR_PROJECT_ID:anon', 'YOUR_PROJECT_ID:restricted'],
});`,
diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx
index 92075c83b9..331a9dc475 100644
--- a/docs/content/docs/(guides)/concepts/jwt.mdx
+++ b/docs/content/docs/(guides)/concepts/jwt.mdx
@@ -86,7 +86,7 @@ Anonymous user tokens have the same shape, but:
Restricted user tokens (e.g., users who haven't verified their email when verification is required) have:
-- `iss` remains unchanged (same as regular users)
+- `iss` becomes `https://api.stack-auth.com/api/v1/projects-restricted-users/`
- `aud` becomes `:restricted`
- `is_restricted` is `true`
- `restricted_reason` is `{ "type": "email_not_verified" }`
From e433f2920c4b98c4a71f917effffc353d664c956 Mon Sep 17 00:00:00 2001
From: "promptless[bot]" <179508745+promptless[bot]@users.noreply.github.com>
Date: Wed, 11 Feb 2026 18:56:32 +0000
Subject: [PATCH 5/5] Sync documentation updates
---
docs/content/docs/(guides)/concepts/jwt.mdx | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/docs/content/docs/(guides)/concepts/jwt.mdx b/docs/content/docs/(guides)/concepts/jwt.mdx
index 331a9dc475..ee1410ebe6 100644
--- a/docs/content/docs/(guides)/concepts/jwt.mdx
+++ b/docs/content/docs/(guides)/concepts/jwt.mdx
@@ -48,8 +48,8 @@ Stack Auth JWTs contain standardized headers and claims that power authenticatio
- **`email_verified`**: Whether the user's email has been verified
- **`selected_team_id`**: The currently selected team ID (nullable)
- **`is_anonymous`**: Whether this is an anonymous user session
-- **`is_restricted`**: Whether the user is restricted (e.g., unverified email or anonymous)
-- **`restricted_reason`**: Why the user is restricted (nullable). The `type` field is either `anonymous` or `email_not_verified`
+- **`is_restricted`**: Whether the user is restricted (e.g., unverified email, anonymous, or restricted by an administrator)
+- **`restricted_reason`**: Why the user is restricted (nullable). The `type` field is `anonymous`, `email_not_verified`, or `restricted_by_administrator`
## Example JWT Payload
@@ -91,6 +91,13 @@ Restricted user tokens (e.g., users who haven't verified their email when verifi
- `is_restricted` is `true`
- `restricted_reason` is `{ "type": "email_not_verified" }`
+Users restricted by an administrator (e.g., via [sign-up rules](/docs/concepts/sign-up-rules)) have the same structure:
+
+- `iss` becomes `https://api.stack-auth.com/api/v1/projects-restricted-users/`
+- `aud` becomes `:restricted`
+- `is_restricted` is `true`
+- `restricted_reason` is `{ "type": "restricted_by_administrator" }`
+
## Working with JWTs
### Client-Side Usage