# Sharing and access

[Permissions](/roles-and-permissions) decide whether you may call an operation. They do
not decide which rows come back. Two mechanisms do that, and they answer different
questions:

- **Row ACLs**: who inside the workspace can reach this particular row.
- **Share links**: how something reaches someone who has no account at all.

Neither weakens the [workspace boundary](/security-and-tenancy). Row-level security
runs first and unconditionally; ACLs narrow within what it already allows.

## Row ACLs

A row that can be shared carries an ACL: a map from subject to the operations that
subject may perform.

```json
{
  "u_3f2a9c1e-7b44-4c0d-9d2f-8a1b6e5c0d31": ["r", "w", "o"],
  "r_8c14e45f-1b22-4a10-9e3d-2f6c1a4b7e90": ["r"]
}
```

| Prefix | Subject |
|---|---|
| `u_` | One person, by user id. |
| `t_` | Everyone in the workspace, by workspace id. |
| `r_` | Everyone holding a role, by role id. |

| Op | Means |
|---|---|
| `r` | Read it. |
| `w` | Read and change it. |
| `o` | Read, change, and decide who else can reach it. |

The operations nest: granting `o` stores `["r","w","o"]`, and granting `w` stores
`["r","w"]`. You do not have to send the implied operations, and reading them back returns
them expanded.

### An ACL is optional, and it only narrows

Most rows carry no ACL, and that is the ordinary state rather than a gap: the row is
governed by permissions alone, so anyone in the workspace holding the permission for an
operation may perform it. Attaching an ACL is how you take one row out of that default and
confine it to the people you name.

It only ever subtracts. Naming someone in an ACL does not hand them access their role and
plan do not already carry. It narrows who, among those who could otherwise reach the row,
still can. The two gates apply in order: the permission decides whether an operation is
open to you at all, and the ACL decides whether this particular row is.

That is why `null` and `{}` both mean "no narrowing applied" rather than "nobody": an ACL
expresses a restriction, so an empty one restricts nothing. Clearing the map is how you
lift a restriction you no longer want, and to impose one you name at least one subject.

### Set an ACL

Rows that support ACLs accept the map on create and update, and designs and infographics
expose it on its own endpoint:

<CodeTabs syncKey="lang">

```bash title="cURL"
curl -X PUT "https://api.genuineai.app/api/v1/designs/{id}/acl" \
  -H "X-Api-Key: gai_…" \
  -H "X-Tenant-Id: <workspace-id>" \
  -H "Content-Type: application/json" \
  -d '{"acl": {"r_8c14e45f-1b22-4a10-9e3d-2f6c1a4b7e90": ["r"]}}'
```

```js title="JavaScript"
await fetch(`https://api.genuineai.app/api/v1/designs/${designId}/acl`, {
  method: "PUT",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    acl: { "r_8c14e45f-1b22-4a10-9e3d-2f6c1a4b7e90": ["r"] },
  }),
});
```

```python title="Python"
requests.put(
    f"https://api.genuineai.app/api/v1/designs/{design_id}/acl",
    headers=headers,
    json={"acl": {"r_8c14e45f-1b22-4a10-9e3d-2f6c1a4b7e90": ["r"]}},
)
```

</CodeTabs>

A malformed map is refused with 422 rather than partially applied: subject keys must be a
known prefix plus a uuid, and operations must come from `r`/`w`/`o`.

### Where the filtering happens

An ACL is applied in the query rather than after it. A list endpoint over ACL-carrying
rows returns only the rows you can read; it does not return everything and mark the rest.
A row you cannot reach is therefore indistinguishable from a row that does not exist, and
a `GET` by id answers **404** rather than 403. That is deliberate, because a 403 would
confirm the row exists.

### Conversations use members, not ACLs

Threads and spaces share through an explicit member list instead:

```
POST   /api/v1/threads/{id}/members       share with someone
GET    /api/v1/threads/{id}/members       see who it is shared with
DELETE /api/v1/threads/{id}/members/{userId}
```

Same idea, different shape, because a conversation has participants rather than readers.

## Share links

A share link reaches people with no account. It is a token in a URL, served from the
public portal, and it is the only way workspace content leaves the authenticated surface.

<CodeTabs syncKey="lang">

```bash title="cURL"
curl -X POST https://api.genuineai.app/api/v1/share-links \
  -H "X-Api-Key: gai_…" \
  -H "X-Tenant-Id: <workspace-id>" \
  -H "Content-Type: application/json" \
  -d '{
        "share_type": "download",
        "name": "Q3 campaign assets",
        "file_ids": ["…"],
        "expires_at": "2026-09-30T00:00:00Z"
      }'
```

```js title="JavaScript"
const res = await fetch("https://api.genuineai.app/api/v1/share-links", {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    share_type: "download",
    name: "Q3 campaign assets",
    file_ids: ["…"],
    expires_at: "2026-09-30T00:00:00Z",
  }),
});
const link = await res.json();
```

```python title="Python"
res = requests.post(
    "https://api.genuineai.app/api/v1/share-links",
    headers=headers,
    json={
        "share_type": "download",
        "name": "Q3 campaign assets",
        "file_ids": ["…"],
        "expires_at": "2026-09-30T00:00:00Z",
    },
)
link = res.json()
```

</CodeTabs>

Links can carry an expiry, a password and caps on how much can be downloaded, and they
count views and downloads so you can see whether one is being used. Creating them requires
`share-link:create`, which a plan holds only with `module-media-sharing`.

Recipients use the unauthenticated endpoints under `/public/share/{token}`: verify the
password first if the link has one, then read or download. Upload links work the same way
in reverse, with `/public/upload/{token}` letting someone contribute files without an
account.

**Treat `share-link:create` as more consequential than other permissions.** Every other
permission on this platform decides what someone can do inside the workspace; this one
decides what leaves it. A revoked link stops working immediately (deleting the share link
is the revocation), but anything already downloaded cannot be recalled.

## Choosing a mechanism

| You want | Use |
|---|---|
| Some people in the workspace should not see this row | A row ACL |
| Some people should read but not change it | An ACL with `r` |
| A client with no account needs the files | A share link |
| Someone outside needs to contribute files | An upload link |
| A conversation should include specific colleagues | Thread members |

Using a share link where an ACL would do publishes something that did not need
publishing. Using an ACL where a share link is needed usually ends with someone emailing a
zip file, which is worse.

<Callout type="note">
	The product guide covers the same subject from the app's side: [the "Who can use this" control these ACLs sit behind, and when a share link is the right answer instead](/guide/admin/sharing-and-access).
</Callout>
