> ## Documentation Index
> Fetch the complete documentation index at: https://docs.postbreeze.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Workspaces

> Multi-tenant isolation for posts, accounts, media, and team.

A **workspace** is the top-level tenant in Postbreeze. Each workspace
owns:

* Its own set of connected social accounts (one Instagram account, one
  TikTok handle, etc.)
* Its own posts, drafts, and publish history
* Its own media library and folders
* Its own team (owner, admins, editors, client reviewers)

Workspaces never share data. Every API call resolves to exactly one
workspace; the auth layer rejects cross-workspace traffic.

## API key access models

When you mint a key in the dashboard you pick between two access
modes:

* **Full access** (default) — the key can act on every workspace its
  owner is a member of. A new workspace the owner joins later is
  automatically reachable.
* **Scoped** — the key is restricted to an explicit allow-list of
  workspaces. Toggle "Full access" off in the create-key sheet and
  pick the workspaces from the list. Any call targeting a workspace
  outside the allow-list returns `403`.

Both modes work the same way at the call site. The SDK's resource
calls (e.g. `postbreeze.posts.get`, `postbreeze.media.move`) derive the workspace
from the referenced row server-side — you never have to look it up
client-side.

## Workspace inference

How the workspace gets resolved on each call:

| Call shape                                                               | Source of `workspaceId`                   |
| ------------------------------------------------------------------------ | ----------------------------------------- |
| `postbreeze.posts.get({ postId })`                                       | Derived from `Post.workspaceId`           |
| `postbreeze.posts.create({ platforms: [{ accountId }] })`                | Derived from the first account            |
| `postbreeze.socialAccounts.disconnect({ accountId })`                    | Derived from `SocialAccount.workspaceId`  |
| `postbreeze.media.get({ mediaId })`                                      | Derived from the media's owner workspace  |
| `postbreeze.posts.list()` / `postbreeze.posts.list({ workspaceId })`     | Optional — omit to fan out, pass to scope |
| `postbreeze.socialAccounts.list()` / `.list({ workspaceId })`            | Optional — same                           |
| `postbreeze.comments.list()` / `.list({ workspaceId })`                  | Optional — same                           |
| `postbreeze.media.list({ workspaceId })`                                 | **Required** (account-global)             |
| `postbreeze.media.presign({ workspaceId, … })` / `.ingestFromUrl({ … })` | **Required**                              |

**Fan-out lists** (no `workspaceId`) query every workspace the key can
act on and return a single merged list, newest-first. Each row carries
its own `workspaceId` so callers can group or filter client-side.

Resource calls don't need a workspaceId either: the server reads it
off the referenced row. The SDK accepts an optional `workspaceId` on
those for client-side defense-in-depth ("assert that this post lives
in workspace X") but it's purely informational.

## Multi-brand example

A single full-access key drives every workspace. The flat list returns
every account across every workspace the key can reach:

```ts theme={null}
import Postbreeze from "@postbreeze/node";

const postbreeze = new Postbreeze({ apiKey: process.env.POSTBREEZE_API_KEY! });

// One call — every connected account everywhere the key can act on.
const accounts = await postbreeze.socialAccounts.list();

// Group client-side by workspace if you want.
const byWorkspace = Map.groupBy(accounts, (a) => a.workspaceId);

await postbreeze.posts.create({
  content: "Acme launch",
  platforms: [{ accountId: byWorkspace.get("wsp_acme")![0].id }],
});
await postbreeze.posts.create({
  content: "Globex launch",
  platforms: [{ accountId: byWorkspace.get("wsp_globex")![0].id }],
});

// Or pass `workspaceId` to scope the list to one workspace explicitly:
const acmeOnly = await postbreeze.socialAccounts.list({ workspaceId: "wsp_acme" });
```

For tighter security in CI / production, mint a **scoped** key that
only sees the workspaces it needs. Leakage of a scoped key is bounded
to its allow-list; a leaked full-access key compromises every
workspace the owner is in.

## Owner vs. member workspaces

If you were invited to a workspace, the **owner** controls its billing
plan and feature access. You can still create posts, schedule, and use
the API — but plan-gated features (advanced analytics, white-label, the
posts-per-month cap) are evaluated against the owner's subscription.

The dashboard surfaces a "Joined" badge on workspaces you don't own.
