docs: teams recipe and playground examples (PUT-1966) (#3967)
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s

* docs: add the easy teams recipe and playground examples

Covers the read-only surface an app actually has: list(), listMembers()
and listDirectory(). Adds the teams recipe tag the build validates
against, and a Teams section to the playground.

Each example handles the two states that are easy to confuse: teams
turned off on the deployment (not_found) and a team that has not opened
its directory to apps (team_not_found).

* docs: open the teams recipe with what it lets an app build

* refine

---------

Co-authored-by: Reynaldi Chernando <reynaldichernando@gmail.com>
This commit is contained in:
Juan Fernando CastroandReynaldi Chernando authored and GitHub committed 2026-10-01 00:11:42 +07:00
1 parent b9efac2363
commit 3d1357455f
9 files changed
+213 -115

No files matched your search

+2 -3
View File
@@ -14,7 +14,7 @@ A team is a Puter account that pays for other accounts. Members are ordinary Put
**Team context.** `list()` tells you whether the signed-in user belongs to a team, and is also how you detect whether the deployment has Teams at all: it rejects with `not_found` where the feature is off, and resolves to an empty array where it is on and the user has no team. Called from an app, it only includes teams whose owner opened the directory to apps (see below); others are simply omitted.
**Member and colleague lookup.** `listDirectory()` returns the team's active members so your app can suggest people by name instead of asking users to type usernames. `listMembers()` returns the same roster; called with the user's own session or API token it also carries `orgOwned` and `createdAt`, but an app only ever gets `username`. Both are opt-in per team: until an owner opens the directory to apps, an app gets `team_not_found`, which is indistinguishable from having no team.
**Member and colleague lookup.** `listDirectory()` returns the team's active members so your app can suggest people by name instead of asking users to type usernames. It is opt-in per team: until an owner opens the directory to apps, an app gets `team_not_found`, which is indistinguishable from having no team.
**Sharing with a team.** Anything shared with a team reaches every member with one grant, including anyone added later. Pass the team's `uid` as the recipient of [`puter.fs.share()`](/FS/share/); there is no string form, since a bare string is always read as an email or username.
@@ -23,10 +23,9 @@ A team is a Puter account that pays for other accounts. Members are ordinary Put
## Functions
- **[`puter.teams.list()`](/Teams/list/)** - List the teams the signed-in user belongs to, and detect whether Teams is available at all
- **[`puter.teams.listMembers()`](/Teams/listMembers/)** - List a team's accounts, with owner-only seat details
- **[`puter.teams.listDirectory()`](/Teams/listDirectory/)** - List a team's members, where the owner has opened the directory to apps
All three are keyset-paginated and take the same options; see each method page for the paging forms and the full error list.
Both are keyset-paginated and take the same options; see each method page for the paging forms and the full error list.
## Examples
+1 -2
View File
@@ -10,8 +10,7 @@ Returns the team's member directory — the colleagues of the user your app is
running for, active accounts only. It is consent-gated: the team's owner has to
open the directory to apps, and until they do it rejects with `team_not_found`
for every caller, indistinguishable from the team not existing. The same
consent decides whether an app sees the team in [`list()`](/Teams/list/) and
may call [`listMembers()`](/Teams/listMembers/).
consent decides whether an app sees the team in [`list()`](/Teams/list/).
The membership is always the signed-in user's, never the app's: an app can only
see the directory of a team its user belongs to.
-102
View File
@@ -1,102 +0,0 @@
---
title: puter.teams.listMembers()
description: List a team's accounts.
platforms: [websites, apps, nodejs, workers]
---
<div class="info">The Teams API is in beta. Method shapes, limits, and behavior may change between releases.</div>
Returns a team's accounts, owner included, whether provisioned by the team or
joined from an existing account.
Called from an app, it needs the same consent as
[`listDirectory()`](/Teams/listDirectory/): until the team's owner opens the
directory to apps it rejects with `team_not_found`, indistinguishable from the
team not existing. An app also sees only active accounts: suspended seats and
seats that have not activated yet are left out, and each `TeamMember` carries
only `username` — no `orgOwned`, `createdAt`, or `uuid`. Called with the
user's own session or API token, it lists every account whatever the
directory setting, with the full `TeamMember` shape.
The membership is always the signed-in user's, never the app's: an app can
only see the members of a team its user belongs to.
## Syntax
```js
puter.teams.listMembers(uid)
puter.teams.listMembers(uid, options)
```
## Parameters
#### `uid` (String) (required)
The team's `uid`, from [`list()`](/Teams/list/).
#### `options` (Object) (optional)
The standard list options. All four are optional, and they decide the shape of what resolves:
| Call | Resolves to |
| -- | -- |
| No options | The whole set as an array, fetched page by page under the hood |
| `{ limit }` | An array, capped at one page |
| `{ cursor }` or `{ includeTotal: true }` | One `{ items, cursor? }` page. `cursor` is absent on the last page |
| `{ stream: true }` | An async iterator of `{ items, cursor? }` pages |
This route is keyset-paginated, so `offset` is not accepted — passing it throws `invalid_request`. Pass `cursor` to resume from a position.
## Return value
A `Promise` that resolves to an array of
`TeamMember` objects, or to a
`{ items, cursor? }` page when a pagination option is given. With
`stream: true` it returns an async iterator of pages instead.
#### `TeamMember`
| Field | Type | Description |
| -- | -- | -- |
| `username` | `string` | The member's Puter username. |
| `orgOwned` | `boolean` (optional) | Whether the team provisioned and pays for this account, as opposed to a pre-existing account that joined it. Absent when an app calls: it gets `username` only. |
| `createdAt` | `string` (optional) | When the account joined the team. Absent when an app calls. |
| `uuid` | `string` (optional) | The account's stable identifier. Present only when the team owner calls with their own session or API token. |
## Errors
A rejection carries an `Error` with a stable `code`:
| Code | Meaning |
| -- | -- |
| `invalid_request` | Refused before reaching the server — a blank `uid`, or an `offset` on a keyset list. |
| `token_missing` | No authentication token was presented. |
| `token_auth_failed` | The token presented did not authenticate. |
| `forbidden` | Called with a scoped access token. |
| `account_is_not_verified` | The caller's email has not been confirmed. |
| `not_found` | Teams are turned off on this deployment. |
| `team_not_found` | No such team, the caller is not a member of it, or — for an app — the owner has not opened the directory to apps. |
| `too_many_requests` | The rate limit was exceeded. See [Rate Limits & Quotas](/rate-limits-and-quotas/). |
## Examples
<strong class="example-title">List a team's accounts</strong>
```html
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
const [team] = await puter.teams.list();
if ( ! team ) return puter.print('Not on a team');
const members = await puter.teams.listMembers(team.uid);
for (const member of members) {
puter.print(`${member.username}${member.orgOwned ? ' (seat)' : ''}<br>`);
}
})();
</script>
</body>
</html>
```
+17
View File
@@ -953,6 +953,23 @@ const examples = [
},
],
},
{
title: 'Teams',
children: [
{
title: "Find the user's team",
description: "Check whether the signed-in user belongs to a team with Puter.js teams API. Run and experiment with this example directly in the playground.",
slug: 'teams-list',
source: '/playground/examples/teams-list.html',
},
{
title: 'Offer colleagues in a picker',
description: 'Build a colleague picker from a team directory with Puter.js teams API. Run and experiment with this example in the playground.',
slug: 'teams-directory',
source: '/playground/examples/teams-directory.html',
},
],
},
{
title: 'Workers',
children: [
@@ -0,0 +1,48 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<select id="colleagues"><option>Loading…</option></select>
<button id="pick">Pick</button>
<script>
const select = document.getElementById('colleagues');
(async () => {
let teams;
try {
teams = await puter.teams.list();
} catch (e) {
if (e.code !== 'not_found') throw e;
select.innerHTML = '<option>Teams are not available here</option>';
return;
}
const team = teams[0];
if (!team) {
select.innerHTML = '<option>You are not on a team</option>';
return;
}
// Off by default, and the usual reason the list comes back empty.
if (!team.directoryEnabled) {
select.innerHTML = '<option>Directory is closed to apps</option>';
return;
}
const entries = await puter.teams.listDirectory(team.uid);
select.innerHTML = '';
for (const { username, uuid } of entries) {
const option = document.createElement('option');
// Store the uuid: it survives a username change.
option.value = uuid;
option.textContent = username;
select.append(option);
}
})();
document.getElementById('pick').addEventListener('click', () => {
const option = select.selectedOptions[0];
if (option) puter.print(`${option.textContent} → ${option.value}<br>`);
});
</script>
</body>
</html>
@@ -0,0 +1,31 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
let teams;
try {
teams = await puter.teams.list();
} catch (e) {
// No /teams route at all when the feature is off, so this
// rejects rather than returning an empty list.
if (e.code !== 'not_found') throw e;
puter.print('Teams are not available on this Puter.');
return;
}
if (teams.length === 0) {
puter.print('You are not on a team.');
return;
}
for (const team of teams) {
puter.print(`${team.name ?? team.handle ?? 'Unnamed team'}<br>`);
puter.print(`uid: ${team.uid}<br>`);
puter.print(`owner: ${team.isOwner}<br>`);
puter.print(`directory open to apps: ${team.directoryEnabled}<br><br>`);
}
})();
</script>
</body>
</html>
+1
View File
@@ -13,6 +13,7 @@ const recipeTags = {
hosting: 'Hosting',
workers: 'Workers',
ui: 'UI',
teams: 'Teams',
performance: 'Performance',
'data-modeling': 'Data Modeling',
};
+113
View File
@@ -0,0 +1,113 @@
---
title: Add Team Support
description: "Learn how to add team support with Puter.js, so your app can get the user's team and its members."
tags: [teams, auth]
order: 60
---
<div class="info">The Teams API is in beta. Method shapes, limits, and behavior may change between releases.</div>
On Puter, a user can create a team and add accounts to it. The user who creates
the team becomes its owner. When someone on a
team signs in to your app, you can read which team they belong to and who else
is on it. This lets your app greet them with their team's name and suggest
colleagues in places like an assignee dropdown or a share dialog, instead of
asking for a typed username.
## Check whether the user is on a team
To get the teams the signed-in user belongs to, call
[`list()`](/Teams/list/). An empty array means the user is not on a team:
```js
const teams = await puter.teams.list();
if (teams.length === 0) {
console.log('Not on a team');
} else {
console.log(`On ${teams.length} team(s)`);
}
```
## Show which team
Each team has a `name` and a `handle`, and either one may be `null`:
```js
const [team] = await puter.teams.list();
console.log(team.name ?? team.handle ?? 'Unnamed team');
console.log(team.createdAt); // '2026-09-28T10:04:00Z'
console.log(team.isOwner); // true if this user owns the team
```
To refer to a team, use `team.uid`. The
[`listDirectory()`](/Teams/listDirectory/) method takes it, and it is the value
to store. The owner can rename the `handle`, and a deleted team's
handle becomes free for another team to take, so only `uid` always points to
the same team.
## List the users in a team
To get the active users in a team, call
[`listDirectory()`](/Teams/listDirectory/) with the team's `uid`. Each entry has a `username` and a `uuid`. The `uuid`
stays the same when the user changes their username, so store the `uuid` if
you save a reference to someone:
```js
const entries = await puter.teams.listDirectory(team.uid);
for (const { username, uuid } of entries) {
console.log(username, uuid);
}
```
With this list, your app can suggest team members wherever the user picks
people. For example, when the user [shares a file](/recipes/share-a-file/),
your app can suggest usernames from their team, so the user does not have to
type a username from memory.
## Check that the team has turned on its directory
Your app can see a team only after the team owner turns on its directory. It is
off by default. Until the owner turns it on, [`list()`](/Teams/list/) leaves
the team out of its result, and
[`listDirectory()`](/Teams/listDirectory/) rejects with `team_not_found`. The
same calls made from the user's own Puter session still return the team.
Each team has a `directoryEnabled` flag. Check it before listing the users in a team, so
you can tell the user what to do:
```js
const [team] = await puter.teams.list();
if (team.directoryEnabled) {
const entries = await puter.teams.listDirectory(team.uid);
console.log(entries);
} else {
console.log('Ask the team owner to turn on the directory.');
}
```
## List a large team
By default, [`listDirectory()`](/Teams/listDirectory/) returns the whole list
as an array. For a large team, pass `stream: true` to read it one page at a
time. Each page has an `items` array:
```js
for await (const page of puter.teams.listDirectory(team.uid, { stream: true })) {
for (const { username } of page.items) console.log(username);
}
```
To load one page at a time yourself, such as behind a "Load more" button, pass
`limit` and `cursor`. The result has a `cursor` for the next page while more
pages remain:
```js
const page = await puter.teams.listDirectory(team.uid, { limit: 50, cursor: null });
console.log(page.items);
console.log(page.cursor);
```
-8
View File
@@ -1199,14 +1199,6 @@ let sidebar = [
source: '/Teams/list.md',
path: '/Teams/list',
},
{
title: '<code>listMembers()</code>',
page_title: '<code>puter.teams.listMembers()</code>',
title_tag: 'puter.teams.listMembers()',
icon: '/assets/img/function.svg',
source: '/Teams/listMembers.md',
path: '/Teams/listMembers',
},
{
title: '<code>listDirectory()</code>',
page_title: '<code>puter.teams.listDirectory()</code>',