diff --git a/content/docs/auth/guides/plugins.md b/content/docs/auth/guides/plugins.md
index 834cb55389..c5cb49cf0c 100644
--- a/content/docs/auth/guides/plugins.md
+++ b/content/docs/auth/guides/plugins.md
@@ -6,7 +6,7 @@ summary: >-
and management through the Neon SDK without direct installation or
configuration by users.
enableTableOfContents: true
-updatedOn: '2026-05-12T23:02:23.681Z'
+updatedOn: '2026-05-27T14:28:53.887Z'
---
@@ -23,15 +23,15 @@ The following Better Auth plugins are currently supported in Neon Auth:
## Supported plugins
-| Plugin | Status |
-| ------------------------------------------------------ | ----------------------------------------------- |
-| [Admin](/docs/auth/guides/plugins/admin) | ✅ Supported |
-| [Email OTP](/docs/auth/guides/plugins/email-otp) | ✅ Supported |
-| [JWT](/docs/auth/guides/plugins/jwt) | ✅ Supported |
-| [Magic Link](/docs/auth/guides/plugins/magic-link) | ✅ Supported |
-| [Organization](/docs/auth/guides/plugins/organization) | ⚠️ Partial (JWT token claims under development) |
-| [Open API](/docs/auth/guides/plugins/openapi) | ✅ Supported |
-| [Phone Number](/docs/auth/guides/plugins/phone-number) | ✅ Supported |
+| Plugin | Status |
+| ------------------------------------------------------ | ------------ |
+| [Admin](/docs/auth/guides/plugins/admin) | ✅ Supported |
+| [Email OTP](/docs/auth/guides/plugins/email-otp) | ✅ Supported |
+| [JWT](/docs/auth/guides/plugins/jwt) | ✅ Supported |
+| [Magic Link](/docs/auth/guides/plugins/magic-link) | ✅ Supported |
+| [Organization](/docs/auth/guides/plugins/organization) | ✅ Supported |
+| [Open API](/docs/auth/guides/plugins/openapi) | ✅ Supported |
+| [Phone Number](/docs/auth/guides/plugins/phone-number) | ✅ Supported |
For more runnable Neon Auth samples, see the [neondatabase/neon-js](https://github.com/neondatabase/neon-js/tree/main/examples) examples repository:
diff --git a/content/docs/auth/guides/plugins/jwt.md b/content/docs/auth/guides/plugins/jwt.md
index 3cabab21e0..1c9d4c94c5 100644
--- a/content/docs/auth/guides/plugins/jwt.md
+++ b/content/docs/auth/guides/plugins/jwt.md
@@ -7,7 +7,7 @@ summary: >-
domains, while emphasizing that it does not replace session management for
standard web applications.
enableTableOfContents: true
-updatedOn: '2026-02-15T20:51:54.038Z'
+updatedOn: '2026-05-27T14:28:53.887Z'
---
@@ -76,7 +76,6 @@ A typical decoded JWT payload looks like this:
"name": "User Name",
"email": "user@email.com",
"emailVerified": false,
- "image": null,
"createdAt": "2025-12-20T11:04:41.437Z",
"updatedAt": "2025-12-20T11:04:41.437Z",
"role": "authenticated",
@@ -91,6 +90,35 @@ A typical decoded JWT payload looks like this:
}
```
+If the session has an active org (via [`setActive()`](/docs/auth/guides/plugins/organization#set-active-organization)), the payload also includes an `o` claim:
+
+```json
+{
+ "iat": 1766320685,
+ "name": "User Name",
+ "email": "user@email.com",
+ "emailVerified": false,
+ "createdAt": "2025-12-20T11:04:41.437Z",
+ "updatedAt": "2025-12-20T11:04:41.437Z",
+ "role": "authenticated",
+ "banned": false,
+ "banReason": null,
+ "banExpires": null,
+ "id": "",
+ "o": {
+ "id": "",
+ "slug": "acme-corp",
+ "role": "owner"
+ },
+ "sub": "",
+ "exp": 1766321585,
+ "iss": "",
+ "aud": ""
+}
+```
+
+`o.role` is the member's role at token issuance time (`owner`, `admin`, or `member`). The claim is absent when no active organization is set. See [Organization context in JWTs](/docs/auth/guides/plugins/organization#organization-context-in-jwts) for the full flow.
+
## Verify a token
To verify the authenticity of a JWT, you need to validate its signature using the public keys provided by JWKS (JSON Web Key Set).
@@ -314,7 +342,7 @@ Because Neon Auth is a managed service, certain server-side configurations avail
- **Signing algorithm:** Neon Auth uses **EdDSA (Ed25519)** by default for high security and performance. Ensure your verification libraries support this algorithm.
- **Expiration:** Tokens expire in **15 minutes** (access tokens). You should implement logic to refresh the token using `authClient.token()` when it expires.
-- **Custom claims:** Currently, the JWT payload contains the default user information. Custom claims are not supported at this time.
+- **Custom claims:** Not supported. Neon Auth adds an `o` claim automatically when the session has an active org (see [Organization context in JWTs](/docs/auth/guides/plugins/organization#organization-context-in-jwts)).
## Troubleshooting
diff --git a/content/docs/auth/guides/plugins/organization.md b/content/docs/auth/guides/plugins/organization.md
index 286f585f42..405b7444f6 100644
--- a/content/docs/auth/guides/plugins/organization.md
+++ b/content/docs/auth/guides/plugins/organization.md
@@ -6,7 +6,7 @@ summary: >-
including creating organizations, inviting members, and managing permissions
through the Organization plugin APIs.
enableTableOfContents: true
-updatedOn: '2026-04-09T23:15:21.000Z'
+updatedOn: '2026-05-27T23:29:49.973Z'
---
@@ -14,7 +14,7 @@ updatedOn: '2026-04-09T23:15:21.000Z'
Neon Auth is built on [Better Auth](https://www.better-auth.com/) and comes with a pre-configured Organization plugin, so your app can support multi-tenancy without additional setup.
-The Organization plugin is currently in **Beta**. Support for JWT token claims is under development.
+The Organization plugin is currently in **Beta**.
## Why use this plugin?
@@ -239,6 +239,68 @@ const { data, error } = await authClient.organization.setActive({
});
```
+### Organization context in JWTs
+
+With an active organization set, the JWT includes an `o` claim containing the org ID, slug, and the member's role. Downstream services can authorize requests without an extra API call.
+
+```ts
+// 1. Set the active organization for the session
+await authClient.organization.setActive({ organizationId: 'org_12345678' });
+
+// 2. Fetch a JWT — it will now include the o claim
+const { data } = await authClient.token();
+// data.token is a JWT containing:
+// { ..., o: { id: "org_12345678", slug: "acme-corp", role: "owner" } }
+```
+
+**The `o` claim**
+
+| Field | Type | Description |
+| :----- | :----- | :------------------------------------------------------------- |
+| `id` | string | Organization ID |
+| `slug` | string | URL-friendly org identifier |
+| `role` | string | Member's role at token issuance: `owner`, `admin`, or `member` |
+
+**Clearing org context**
+
+Pass `organizationId: null` to remove the active org from the session. The next `token()` call returns a JWT without the `o` claim.
+
+```ts
+await authClient.organization.setActive({ organizationId: null });
+const { data } = await authClient.token();
+// data.token JWT has no o claim
+```
+
+
+The `o.role` value is fixed at issuance. If the member's role changes, the JWT reflects the old role until expiry. For sensitive operations, verify the current role server-side with [`organization.getActiveMember()`](#get-active-member).
+
+
+### Using the org claim for RLS
+
+When you use the [Neon Data API](/docs/data-api/overview), the `o` claim is available inside `auth.jwt()`. Create a helper function to extract it for use in RLS policies:
+
+```sql
+CREATE OR REPLACE FUNCTION public.jwt_organization()
+RETURNS jsonb
+LANGUAGE sql
+STABLE
+SECURITY DEFINER
+SET search_path = public
+AS $$
+ SELECT auth.jwt() -> 'o';
+$$;
+```
+
+Use the helper in policies to scope rows to the active org:
+
+```sql
+CREATE POLICY "Org members can view team data" ON public.items
+ FOR SELECT TO authenticated
+ USING (organization_id = public.jwt_organization() ->> 'id');
+```
+
+For the complete multi-tenant RLS pattern (including personal rows alongside org-scoped rows), see [Multi-tenant access with organizations](/docs/data-api/access-control#multi-tenant-access-with-organizations).
+
### Get active organization
Retrieves full details of the currently active organization.
diff --git a/content/docs/data-api/access-control.md b/content/docs/data-api/access-control.md
index b6ca51f3bb..951f95e5b8 100644
--- a/content/docs/data-api/access-control.md
+++ b/content/docs/data-api/access-control.md
@@ -7,7 +7,7 @@ summary: >-
uses PostgreSQL's security model to enforce role privileges and Row-Level
Security for database access control.
enableTableOfContents: true
-updatedOn: '2026-04-18T12:27:58.000Z'
+updatedOn: '2026-05-27T23:29:49.973Z'
---
@@ -17,6 +17,7 @@ updatedOn: '2026-04-18T12:27:58.000Z'
Getting started with Data API
Custom authentication providers
Secure your app with RLS
+ Neon Auth Organizations
@@ -153,3 +154,76 @@ CREATE TABLE posts (
```
Now, even though the `authenticated` role has `SELECT` permission on the table, the database will only return rows where the `user_id` column matches the ID in the user's token.
+
+## Multi-tenant access with organizations
+
+When using [Neon Auth Organizations](/docs/auth/guides/plugins/organization), the JWT includes an `o` claim containing the active org ID, slug, and member role. You can use this claim in RLS policies to scope rows to an organization.
+
+The active org is set client-side with [`authClient.organization.setActive()`](/docs/auth/guides/plugins/organization#set-active-organization). After `setActive()`, the next JWT carries the `o` claim. The Data API enforces org scope via RLS.
+
+### Table schema
+
+Add an `organization_id` column to tables that need org-scoped access:
+
+```sql
+CREATE TABLE public.items (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id text NOT NULL,
+ organization_id text, -- NULL = personal row; org ID = team row
+ content text NOT NULL,
+ created_at timestamptz NOT NULL DEFAULT now()
+);
+
+ALTER TABLE public.items ENABLE ROW LEVEL SECURITY;
+```
+
+### Helper function
+
+Create a helper that extracts the `o` claim from the current JWT:
+
+```sql
+CREATE OR REPLACE FUNCTION public.jwt_organization()
+RETURNS jsonb
+LANGUAGE sql
+STABLE
+SECURITY DEFINER
+SET search_path = public
+AS $$
+ SELECT auth.jwt() -> 'o';
+$$;
+```
+
+### RLS policies
+
+These policies allow two kinds of rows: org-scoped team data (matched by `o.id`) and personal rows (`organization_id IS NULL`, owned by the user):
+
+```sql
+-- jwt_organization() returns NULL when no org is active;
+-- the IS NOT NULL guard prevents false matches against real org IDs
+CREATE POLICY "Authenticated users can view items" ON public.items
+ FOR SELECT TO authenticated USING (
+ (organization_id IS NOT NULL AND organization_id = public.jwt_organization() ->> 'id')
+ OR (organization_id IS NULL AND user_id = auth.user_id())
+ );
+
+CREATE POLICY "Authenticated users can create items" ON public.items
+ FOR INSERT TO authenticated WITH CHECK (
+ user_id = auth.user_id()
+ AND (
+ (organization_id IS NOT NULL AND organization_id = public.jwt_organization() ->> 'id')
+ OR organization_id IS NULL
+ )
+ );
+
+CREATE POLICY "Authenticated users can update items" ON public.items
+ FOR UPDATE TO authenticated USING (
+ (organization_id IS NOT NULL AND organization_id = public.jwt_organization() ->> 'id')
+ OR (organization_id IS NULL AND user_id = auth.user_id())
+ );
+
+CREATE POLICY "Authenticated users can delete items" ON public.items
+ FOR DELETE TO authenticated USING (
+ (organization_id IS NOT NULL AND organization_id = public.jwt_organization() ->> 'id')
+ OR (organization_id IS NULL AND user_id = auth.user_id())
+ );
+```
diff --git a/content/docs/extensions/pg_session_jwt.md b/content/docs/extensions/pg_session_jwt.md
index e006b5c179..1838eb8920 100644
--- a/content/docs/extensions/pg_session_jwt.md
+++ b/content/docs/extensions/pg_session_jwt.md
@@ -6,7 +6,7 @@ summary: >-
authenticated sessions using JSON Web Tokens (JWTs), including JWK validation
and PostgREST compatibility for secure user identity handling.
enableTableOfContents: true
-updatedOn: '2026-05-15T10:22:57.192Z'
+updatedOn: '2026-05-27T23:29:49.973Z'
---
@@ -81,7 +81,17 @@ SELECT auth.session();
### auth.jwt()
-Alias for `auth.session()`.
+Alias for `auth.session()`. When using [Neon Auth Organizations](/docs/auth/guides/plugins/organization), the payload includes an `o` claim for the active org. Extract it directly in RLS:
+
+```sql
+-- Full org object (jsonb): {"id": "org_123", "slug": "acme", "role": "owner"} or NULL
+SELECT auth.jwt() -> 'o';
+
+-- Org ID as text, for policy comparisons
+SELECT auth.jwt() -> 'o' ->> 'id';
+```
+
+See [Multi-tenant access with organizations](/docs/data-api/access-control#multi-tenant-access-with-organizations) for the complete RLS pattern.
### auth.uid()