This commit is contained in:
Reynaldi Chernando committed 2026-10-01 00:10:17 +07:00
1 parent 3dc8f1f932
commit 75d3d53db0
8 files changed
+116 -324

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>
```
-6
View File
@@ -962,12 +962,6 @@ const examples = [
slug: 'teams-list',
source: '/playground/examples/teams-list.html',
},
{
title: 'List team members',
description: 'List the accounts belonging to a team with Puter.js teams API. Run and modify this example instantly in your browser.',
slug: 'teams-list-members',
source: '/playground/examples/teams-list-members.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.',
@@ -1,33 +0,0 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
let teams;
try {
teams = await puter.teams.list();
} catch (e) {
if (e.code !== 'not_found') throw e;
puter.print('Teams are not available on this Puter.');
return;
}
const team = teams[0];
if (!team) {
puter.print('You are not on a team.');
return;
}
try {
const members = await puter.teams.listMembers(team.uid);
puter.print(`${members.length} member(s) of ${team.name}:<br>`);
// An app gets `username` only — no email or activation state.
for (const member of members) puter.print(`${member.username}<br>`);
} catch (e) {
if (e.code !== 'team_not_found') throw e;
puter.print('This team has not opened its directory to apps.');
}
})();
</script>
</body>
</html>
+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);
```
@@ -1,170 +0,0 @@
---
title: Read the User's Team
description: "Find out whether the signed-in user belongs to a team, show which one, and list their colleagues so your app can suggest them."
tags: [teams, auth]
order: 60
---
**Use this when** your app should adapt to the team the signed-in user belongs
to rather than treating everyone as a lone user. A few things this unlocks:
- A **task board** that offers colleagues in an assignee dropdown instead of
asking for a typed username.
- A **document editor** whose share dialog suggests the people you actually work
with.
- A **dashboard** that greets someone with their team's name and keeps each
team's data under its own key.
- An **onboarding screen** that can tell "you have no team yet" apart from "this
Puter has no teams at all" — and say something useful either way.
Everything here is read-only. Creating a team, adding accounts to it and paying
for them are done by the team owner from their own Puter account, never from an
app, so `puter.teams` gives an app three calls: [`list()`](/Teams/list/),
[`listMembers()`](/Teams/listMembers/) and
[`listDirectory()`](/Teams/listDirectory/).
## Check whether the user is on a team
`list()` returns the teams the signed-in user belongs to:
```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)`);
}
```
An empty array means *this user has no team*. It does not mean the deployment
has no teams: where the feature is turned off there is no `/teams` route at
all, so the call rejects with `not_found` instead of resolving. Handle the two
separately, or a user on a Puter without teams looks identical to one who
simply has not joined anything:
```js
let teams = [];
try {
teams = await puter.teams.list();
} catch (e) {
if (e.code !== 'not_found') throw e;
// Teams aren't available here — hide the team parts of your UI.
}
```
Failures reject with an error carrying a `code`, so `e.code` is what you branch
on.
## Show which team
A team carries a `name` and a `handle`, and either 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
```
Pass `team.uid` to the other two methods, and store that if you store anything.
Never key on `handle`: it is a label the owner can rename, and deleting a team
releases it for someone else to take, so a saved handle can later resolve to a
different team. `uid` is the stable reference.
## List the user's colleagues
`listMembers()` takes the team's `uid`. Any member may call it:
```js
const [team] = await puter.teams.list();
const members = await puter.teams.listMembers(team.uid);
for (const member of members) {
console.log(member.username);
}
```
From an app each entry carries `username` and nothing else — no email, no
activation state, no usage. The extra fields you will see in the type
(`orgOwned`, `createdAt`, `uuid`) are filled in only when the user's own Puter
session makes the call, so write your UI against `username` alone.
## Offer colleagues in a picker
`listDirectory()` is the one built for suggesting people. It returns each
colleague's `username` together with a `uuid` that survives a rename, which is
what you want to store against a share or an assignment:
```js
const entries = await puter.teams.listDirectory(team.uid);
for (const { username, uuid } of entries) {
addOption(username, uuid);
}
```
Accounts that are suspended, or that never signed in for the first time, are
left out, so the list is people your user can actually reach.
## The team has to opt in
An app sees a team only once that team's owner has turned its directory on.
This is off by default, and it is the single most common reason these calls
come back empty from an app but full from the user's own Puter session.
The behaviour differs per call, which is worth knowing when you are debugging:
- `list()` **omits** teams that have not opted in. It does not throw — a user
on one closed team gets `[]`.
- `listMembers()` and `listDirectory()` **reject** with `team_not_found`.
`directoryEnabled` on the team tells you which case you are in, so you can say
something useful instead of showing an empty list:
```js
const [team] = await puter.teams.list();
if (!team.directoryEnabled) {
console.log('Ask the team owner to turn on the directory.');
} else {
const entries = await puter.teams.listDirectory(team.uid);
}
```
## Longer lists
All three methods share the same three forms. By default you get the whole list
as an array, which is what every example above uses:
```js
const members = await puter.teams.listMembers(team.uid);
```
Passing `cursor` or `includeTotal` switches to a page envelope instead:
```js
const page = await puter.teams.listMembers(team.uid, { limit: 50, cursor: null });
page.items; // this page's members
page.cursor; // present only while more pages remain
```
And `stream: true` hands back an async iterator of those envelopes, for walking
a large team without holding it all at once:
```js
for await (const page of puter.teams.listMembers(team.uid, { stream: true })) {
for (const member of page.items) console.log(member.username);
}
```
## Notes
- These routes are keyset-paginated, so `offset` is rejected rather than
silently ignored. Pass `cursor` to resume from a position.
- Everything is scoped to the signed-in person's own membership, so an app
installed by a member of one team can never read another team.
- A user may belong to more than one team. `list()[0]` is fine for a demo, but
let the user choose if your app acts on a specific one.
-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>',