mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-12 06:41:55 +00:00
docs: teams recipe and playground examples (PUT-1966) (#3967)
* 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:
1 parent
b9efac2363
commit
3d1357455f
9 files changed
+213
-115
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
@@ -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>
|
||||
@@ -13,6 +13,7 @@ const recipeTags = {
|
||||
hosting: 'Hosting',
|
||||
workers: 'Workers',
|
||||
ui: 'UI',
|
||||
teams: 'Teams',
|
||||
performance: 'Performance',
|
||||
'data-modeling': 'Data Modeling',
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
```
|
||||
@@ -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>',
|
||||
|
||||
Reference in new issue
Block a user