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/jwt.ts b/docs/code-examples/concepts/jwt.ts
new file mode 100644
index 0000000000..ec6baca1d4
--- /dev/null
+++ b/docs/code-examples/concepts/jwt.ts
@@ -0,0 +1,140 @@
+import { CodeExample } from '../../lib/code-examples';
+
+export const jwtExamples = {
+ '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')
+);
+
+// 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'],
+});`,
+ 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..ee1410ebe6 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, 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
@@ -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,22 @@ 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:
+
+- `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" }`
+
+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
@@ -84,85 +104,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')
-);
+To support anonymous sessions, include those keys and allow both issuers and audiences:
-// 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);
-}
-```
+To support restricted users (e.g., users who haven't verified their email), add `include_restricted=true`:
-To support anonymous sessions, include those keys and allow both issuers and audiences:
-
-```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