diff --git a/apps/public/content/docs/(tracking)/sdks/express.mdx b/apps/public/content/docs/(tracking)/sdks/express.mdx
index 4160dee2..ab0fa111 100644
--- a/apps/public/content/docs/(tracking)/sdks/express.mdx
+++ b/apps/public/content/docs/(tracking)/sdks/express.mdx
@@ -68,6 +68,34 @@ app.listen(3000, () => {
- `trackRequest` - A function that returns `true` if the request should be tracked.
- `getProfileId` - A function that returns the profile ID of the user making the request.
+## Working with Groups
+
+Groups let you track analytics at the account or company level. Since Express is a backend SDK, you can upsert groups and assign users from your route handlers.
+
+See the [Groups guide](/docs/get-started/groups) for the full walkthrough.
+
+```ts
+app.post('/login', async (req, res) => {
+ const user = await loginUser(req.body);
+
+ // Identify the user
+ req.op.identify({ profileId: user.id, email: user.email });
+
+ // Create/update the group entity
+ req.op.upsertGroup({
+ id: user.organizationId,
+ type: 'company',
+ name: user.organizationName,
+ properties: { plan: user.plan },
+ });
+
+ // Assign the user to the group
+ req.op.setGroup(user.organizationId);
+
+ res.json({ ok: true });
+});
+```
+
## Typescript
If `req.op` is not typed you can extend the `Request` interface.
diff --git a/apps/public/content/docs/(tracking)/sdks/javascript.mdx b/apps/public/content/docs/(tracking)/sdks/javascript.mdx
index 612eac7f..1c35d407 100644
--- a/apps/public/content/docs/(tracking)/sdks/javascript.mdx
+++ b/apps/public/content/docs/(tracking)/sdks/javascript.mdx
@@ -116,9 +116,38 @@ op.decrement({
});
```
+### Working with Groups
+
+Groups let you track analytics at the account or company level. See the [Groups guide](/docs/get-started/groups) for the full walkthrough.
+
+**Create or update a group:**
+
+```js title="index.js"
+import { op } from './op.ts'
+
+op.upsertGroup({
+ id: 'org_acme',
+ type: 'company',
+ name: 'Acme Inc',
+ properties: { plan: 'enterprise' },
+});
+```
+
+**Assign the current user to a group** (call after `identify`):
+
+```js title="index.js"
+import { op } from './op.ts'
+
+op.setGroup('org_acme');
+// or multiple groups:
+op.setGroups(['org_acme', 'team_eng']);
+```
+
+Once set, all subsequent `track()` calls will automatically include the group IDs.
+
### Clearing User Data
-To clear the current user's data:
+To clear the current user's data (including groups):
```js title="index.js"
import { op } from './op.ts'
diff --git a/apps/public/content/docs/(tracking)/sdks/nextjs.mdx b/apps/public/content/docs/(tracking)/sdks/nextjs.mdx
index 3877d1b6..bd01a859 100644
--- a/apps/public/content/docs/(tracking)/sdks/nextjs.mdx
+++ b/apps/public/content/docs/(tracking)/sdks/nextjs.mdx
@@ -227,9 +227,32 @@ useOpenPanel().decrement({
});
```
+### Working with Groups
+
+Groups let you track analytics at the account or company level. See the [Groups guide](/docs/get-started/groups) for the full walkthrough.
+
+**Create or update a group:**
+
+```tsx title="app/login/page.tsx"
+useOpenPanel().upsertGroup({
+ id: 'org_acme',
+ type: 'company',
+ name: 'Acme Inc',
+ properties: { plan: 'enterprise' },
+});
+```
+
+**Assign the current user to a group** (call after `identify`):
+
+```tsx title="app/login/page.tsx"
+useOpenPanel().setGroup('org_acme');
+```
+
+Once set, all subsequent `track()` calls will automatically include the group IDs.
+
### Clearing User Data
-To clear the current user's data:
+To clear the current user's data (including groups):
```js title="index.js"
useOpenPanel().clear()
diff --git a/apps/public/content/docs/(tracking)/sdks/react.mdx b/apps/public/content/docs/(tracking)/sdks/react.mdx
index 9c02df32..6a8834f9 100644
--- a/apps/public/content/docs/(tracking)/sdks/react.mdx
+++ b/apps/public/content/docs/(tracking)/sdks/react.mdx
@@ -174,9 +174,37 @@ function MyComponent() {
}
```
+### Working with Groups
+
+Groups let you track analytics at the account or company level. See the [Groups guide](/docs/get-started/groups) for the full walkthrough.
+
+```tsx
+import { op } from '@/openpanel';
+
+function LoginComponent() {
+ const handleLogin = async (user: User) => {
+ // 1. Identify the user
+ op.identify({ profileId: user.id, email: user.email });
+
+ // 2. Create/update the group entity (only when data changes)
+ op.upsertGroup({
+ id: user.organizationId,
+ type: 'company',
+ name: user.organizationName,
+ properties: { plan: user.plan },
+ });
+
+ // 3. Link the user to their group — tags all future events
+ op.setGroup(user.organizationId);
+ };
+
+ return ;
+}
+```
+
### Clearing User Data
-To clear the current user's data:
+To clear the current user's data (including groups):
```tsx
import { op } from '@/openpanel';
diff --git a/apps/public/content/docs/get-started/groups.mdx b/apps/public/content/docs/get-started/groups.mdx
new file mode 100644
index 00000000..ddc1823a
--- /dev/null
+++ b/apps/public/content/docs/get-started/groups.mdx
@@ -0,0 +1,208 @@
+---
+title: Groups
+description: Track analytics at the account, company, or team level — not just individual users.
+---
+
+import { Callout } from 'fumadocs-ui/components/callout';
+
+Groups let you associate users with a shared entity — like a company, workspace, or team — and analyze behavior at that level. Instead of asking "what did Jane do?", you can ask "what is Acme Inc doing?"
+
+This is especially useful for B2B SaaS products where a single paying account has many users.
+
+## How Groups work
+
+There are two separate concepts:
+
+1. **The group entity** — created/updated with `upsertGroup()`. Stores metadata about the group (name, plan, etc.).
+2. **Group membership** — set with `setGroup()` / `setGroups()`. Links a user profile to one or more groups, and automatically attaches those group IDs to every subsequent `track()` call.
+
+## Creating or updating a group
+
+Call `upsertGroup()` to create a group or update its properties. The group is identified by its `id` and `type`.
+
+```typescript
+op.upsertGroup({
+ id: 'org_acme', // Your group's unique ID
+ type: 'company', // Group type (company, workspace, team, etc.)
+ name: 'Acme Inc', // Display name
+ properties: {
+ plan: 'enterprise',
+ seats: 25,
+ industry: 'logistics',
+ },
+});
+```
+
+### Group payload
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `id` | `string` | Yes | Unique identifier for the group |
+| `type` | `string` | Yes | Category of group (e.g. `"company"`, `"workspace"`) |
+| `name` | `string` | Yes | Human-readable display name |
+| `properties` | `object` | No | Custom metadata about the group |
+
+## Managing groups in the dashboard
+
+The easiest way to create, edit, and delete groups is directly in the OpenPanel dashboard. Navigate to your project and open the **Groups** section — from there you can manage group names, types, and properties without touching any code.
+
+`upsertGroup()` is the right tool when your group properties are **dynamic and driven by your own data** — for example, syncing a customer's current plan, seat count, or MRR from your backend at login time.
+
+
+A good rule of thumb: call `upsertGroup()` on login or when group properties change — not on every request or page view. If you find yourself calling it frequently with the same data, the dashboard is probably the better place to manage that group.
+
+
+## Assigning a user to a group
+
+After identifying a user, call `setGroup()` to link them to a group. This also attaches the group ID to all future `track()` calls for the current session.
+
+```typescript
+// After login
+op.identify({ profileId: 'user_123' });
+
+// Link the user to their organization
+op.setGroup('org_acme');
+```
+
+For users that belong to multiple groups:
+
+```typescript
+op.setGroups(['org_acme', 'team_engineering']);
+```
+
+
+`setGroup()` and `setGroups()` persist group IDs on the SDK instance. All subsequent `track()` calls will automatically include these group IDs until `clear()` is called.
+
+
+## Full login flow example
+
+`setGroup()` doesn't require the group to exist first. You can call it with just an ID — events will be tagged with that group ID, and you can create the group later in the dashboard or via `upsertGroup()`.
+
+```typescript
+// 1. Identify the user
+op.identify({
+ profileId: 'user_123',
+ firstName: 'Jane',
+ email: 'jane@acme.com',
+});
+
+// 2. Assign the user to the group — the group doesn't need to exist yet
+op.setGroup('org_acme');
+
+// 3. All subsequent events are now tagged with the group
+op.track('dashboard_viewed'); // → includes groups: ['org_acme']
+op.track('report_exported'); // → includes groups: ['org_acme']
+```
+
+If you want to sync dynamic group properties from your own data (plan, seats, MRR), add `upsertGroup()` to the flow:
+
+```typescript
+op.identify({ profileId: 'user_123', email: 'jane@acme.com' });
+
+// Sync group metadata from your backend
+op.upsertGroup({
+ id: 'org_acme',
+ type: 'company',
+ name: 'Acme Inc',
+ properties: { plan: 'pro' },
+});
+
+op.setGroup('org_acme');
+```
+
+## Per-event group override
+
+You can attach group IDs to a specific event without affecting the SDK's persistent group state:
+
+```typescript
+op.track('file_shared', {
+ filename: 'q4-report.pdf',
+ groups: ['org_acme', 'org_partner'], // Only applies to this event
+});
+```
+
+Groups passed in `track()` are **merged** with any groups already set on the SDK instance.
+
+## Clearing groups on logout
+
+`clear()` resets the profile, device, session, and all groups. Always call it on logout.
+
+```typescript
+function handleLogout() {
+ op.clear();
+ // redirect to login...
+}
+```
+
+## Common patterns
+
+### B2B SaaS — company accounts
+
+```typescript
+// On login
+op.identify({ profileId: user.id, email: user.email });
+op.upsertGroup({
+ id: user.organizationId,
+ type: 'company',
+ name: user.organizationName,
+ properties: { plan: user.plan, mrr: user.mrr },
+});
+op.setGroup(user.organizationId);
+```
+
+### Multi-tenant — workspaces
+
+```typescript
+// When user switches workspace
+op.upsertGroup({
+ id: workspace.id,
+ type: 'workspace',
+ name: workspace.name,
+});
+op.setGroup(workspace.id);
+```
+
+### Teams within a company
+
+```typescript
+// User belongs to a company and a specific team
+op.setGroups([user.organizationId, user.teamId]);
+```
+
+## API reference
+
+### `upsertGroup(payload)`
+
+Creates the group if it doesn't exist, or merges properties into the existing group.
+
+```typescript
+op.upsertGroup({
+ id: string; // Required
+ type: string; // Required
+ name: string; // Required
+ properties?: Record;
+});
+```
+
+### `setGroup(groupId)`
+
+Adds a single group ID to the SDK's internal group list and sends an `assign_group` event to link the current profile to that group.
+
+```typescript
+op.setGroup('org_acme');
+```
+
+### `setGroups(groupIds)`
+
+Same as `setGroup()` but for multiple group IDs at once.
+
+```typescript
+op.setGroups(['org_acme', 'team_engineering']);
+```
+
+## What to avoid
+
+- **Calling `upsertGroup()` on every event or page view** — call it on login or when group properties actually change. For static group management, use the dashboard instead.
+- **Not calling `setGroup()` after `identify()`** — without it, events won't be tagged with the group and you won't see group-level data in the dashboard.
+- **Forgetting `clear()` on logout** — groups persist on the SDK instance, so a new user logging in on the same session could inherit the previous user's groups.
+- **Using `upsertGroup()` to link a user to a group** — `upsertGroup()` manages the group entity only. Use `setGroup()` to link a user profile to it.
diff --git a/apps/public/content/docs/get-started/meta.json b/apps/public/content/docs/get-started/meta.json
index 4bb85521..d6df8c17 100644
--- a/apps/public/content/docs/get-started/meta.json
+++ b/apps/public/content/docs/get-started/meta.json
@@ -3,6 +3,7 @@
"install-openpanel",
"track-events",
"identify-users",
+ "groups",
"revenue-tracking"
]
}