# Teams

fuast apps are built to be shared with coworkers. Every app lives in a workspace, sharing works like a doc, handlers can list the people in the workspace, and team tables let several apps share one source of truth.

## Workspaces

A workspace is a company. Accounts join it by verifying an email address on the company's domain: everyone who verifies an `@acme.com` address lands in the Acme workspace. Personal email domains such as gmail.com get a workspace of one.

Every app belongs to the workspace of the account that first deployed it. The deploying account is the app's owner. An app deployed without an account is an [unclaimed preview](https://fuast.com/docs/reference.md#deploy-without-an-account): it has no workspace, so `ctx.workspace.members()` and team tables fail there, until a person claims it and it joins their workspace. A claimed app stays [restricted](https://fuast.com/docs/security.md#restricted-apps) until its owner trusts it: team tables and the member list stay off, because every app in a workspace shares its team tables.

### Roles

| Role | Can |
| --- | --- |
| `admin` | Everything a member can, plus manage members and roles, workspace tokens, team tables, and settings, and inspect and manage every app in the workspace (deploys, logs, data, sharing). |
| `member` | Open apps shared with the workspace, deploy their own apps, and create team tables. |

The first verified account in a workspace is its admin. Admins promote others from the dashboard (Workspace, then Members).

App owners and workspace admins can read an app's inspection routes, logs, and data, and change its sharing. Other members can only use the app.

## Sharing

```sh
fuast share --visibility private                         # owner, admins, and the allow-list
fuast share --visibility workspace                       # everyone in the workspace
fuast share --visibility public                          # anyone with the link
fuast share --visibility private --emails ana@acme.com,bo@partner.io
```

`share` targets the app in `fuast.json`, or pass a slug: `fuast share my-app --visibility workspace`.

| Mode | Who can open it | Guests |
| --- | --- | --- |
| `private` | Owner, workspace admins, allow-list. New apps start here. | No |
| `workspace` | Every workspace member, plus the allow-list. | No |
| `public` | Anyone. | Yes, unless `auth.requireSignIn` |

The allow-list is per app and can hold any email, inside or outside the workspace. That is how you share a workspace app with a contractor.

### Invites and access requests

- `--emails` adds addresses to the allow-list and sends each one an invite email with the app link. If they have no account yet, they create one with that address and land in the app.
- Someone who opens an app they cannot use sees a "Request access" page. The request appears in `fuast users list` and the dashboard Sharing tab, and the owner gets an email.

```sh
fuast users list                       # allow-list and pending requests
fuast users approve ana@acme.com       # add to the allow-list (or approve a request)
fuast users deny ana@acme.com          # reject a pending request
fuast users remove ana@acme.com        # take someone off the allow-list
```

Each command takes an optional slug after the email.

Sharing gates the UI and query, mutation, and action calls. It does not gate [endpoints](https://fuast.com/docs/capsule-api.md#endpoints), which stay reachable so webhooks work.

## Workspace members

`ctx.workspace.members()` lists the people in the app's workspace:

```ts
const members = await ctx.workspace.members();
// [{ userId, name, email, picture? }, ...]
```

Use it to pick an assignee, notify a team, or show who can act. It returns the current members every time it is called. It is available in every handler kind except `onGuestUpgrade`. The caller must be a signed-in member of the app's workspace, or a schedule. Anyone else gets an error with code `forbidden`, so guests on a public app cannot list your coworkers.

```ts
queries: {
  assignees: query(async (ctx) => {
    ctx.auth.requireSignedIn();
    const people = await ctx.workspace.members();
    return people.map((p) => ({ userId: p.userId, name: p.name }));
  })
}
```

Return only what the UI needs. Do not send every coworker's email to a client that does not show it.

Member ids are the same `userId` values that `ctx.auth` returns, so you can store them in `userId()` fields:

```ts
tasks: table({ title: string(), assigneeId: userId() }).index("by_assignee", ["assigneeId"])
```

A query that calls `members()` re-runs when its database reads change, not when someone joins the workspace. Reload to see new members.

### Members in `fuast dev`

`fuast dev` has no workspace. There, `members()` returns every signed-in identity that has called the app locally (opened it, or made a query, mutation, or action call), sorted by name. It starts with only the dev user. Guests and `?fuast_anon=1` callers are never listed.

To test an assignee picker with several people, call the app once as each of them, for example by opening `http://localhost:3000/?fuast_user=alice` and `?fuast_user=bob` in the browser, or from the CLI:

```sh
fuast run assignees --as alice
fuast run assignees --as bob
```

Each name becomes a member with `userId` `user_<name>`. They stay listed until `fuast dev --reset`.

## Team tables

Team tables hold rows that every app in the workspace can read and write: a shared customer list, a list of offices, an on-call rota. One app writes, others read, and admins can edit rows by hand in the dashboard (Workspace, then Team tables).

```ts
const customers = ctx.team.table("customers");

const rows = await customers.all();                     // every row, newest first
const id = await customers.insert({ name: "Acme", tier: "gold" });
await customers.update(id, { tier: "platinum" });       // merge a patch
await customers.remove(id);
```

Rows are plain JSON objects plus an `_id` string. A table is created on first insert. There is no schema and no index: validate what you read, and keep each table small (a few thousand rows at most), because `all()` returns everything.

Rules:

- Reads work in every handler. Writes work in mutations, actions, writable endpoints, and schedules.
- Team tables are outside the app's database transaction. A mutation that writes a team row and then throws keeps the team write. Write team rows last.
- Queries that read team tables are not reactive to team writes. If a UI must update live, copy the data into the app's own tables from a mutation or schedule.
- Every app in the workspace sees the same rows, regardless of which app created them. Do not store secrets or one person's private data there.

For data that belongs to one app, use the app's own [database](https://fuast.com/docs/database.md). Reach for team tables only when a second app needs the same rows.

## Putting it together

The [approvals template](https://fuast.com/docs/examples/approvals.md) uses all of this: requests are stored per app, approvers come from `ctx.workspace.members()`, decisions send [email](https://fuast.com/docs/email.md), and a [schedule](https://fuast.com/docs/schedules.md) sends a morning digest.
