---
title: "Sharing & Privacy"
description: "Google-Docs-style sharing, built into the framework. Every user-created resource — docs, dashboards, designs, decks, clips, recordings, forms — gets the same private-by-default model with one consistent share UI."
---

# Sharing & Privacy

Every resource a user creates in an agent-native app — a document, a dashboard, a design, a deck, a screen recording, a meeting transcript, a form, a booking link — is **private to the creator by default**. Other people see it only when the creator explicitly shares it, or changes its visibility to `org` or `public`.

It looks and works like Google Docs. The same share button, the same dialog, the same three-tier visibility model, the same per-user/per-org grants — across every template, with no per-app reinvention.

## Why one model {#why}

Most app frameworks make sharing a per-feature project. The result: every doc-like surface ends up with its own share dialog, its own permissions schema, its own access-check bugs. In agent-native, sharing is a **framework primitive**. The schema columns, the access-check helpers, the share popover, and the agent-callable share actions all ship with the core. A new template gets the full sharing story by adding two columns and one line of registration.

This also means the agent never has to learn a new sharing model per app. Tell the agent "share this with Alice as an editor" in any template and the same `share-resource` action fires.

## The three visibility levels {#visibility}

Coarse visibility lives on the resource itself; fine-grained grants live in a companion shares table.

| Visibility | Who can see it                                                                                      |
| ---------- | --------------------------------------------------------------------------------------------------- |
| `private`  | Owner + people explicitly granted. **Default for every new resource.**                              |
| `org`      | Owner + explicit grants + anyone in the same organization (read-only).                              |
| `public`   | Owner + explicit grants + anyone with the link (read-only). Doesn't appear in others' lists/search. |

`public` is a deliberately quiet level: a public resource is reachable by direct link, but it does **not** show up in other users' sidebars, lists, or search. That keeps "public for sharing the URL" separate from "public for cross-user discovery." Galleries and template catalogs that genuinely want cross-user discovery opt in explicitly.

<Diagram id="doc-block-ajfbtl" title="Visibility, widening outward" summary={"Coarse visibility on the resource sets the floor; explicit share grants in the companion table add named people on top."}>

```html
<div class="share-tiers">
  <div class="diagram-card">
    <span class="diagram-pill">private</span
    ><small class="diagram-muted"
      >owner + explicit grants only &middot; <strong>default</strong></small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill accent">org</span
    ><small class="diagram-muted">+ anyone in the same org (read-only)</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill warn">public</span
    ><small class="diagram-muted"
      >+ anyone with the link (read-only) &middot; hidden from others'
      lists/search</small
    >
  </div>
</div>
```

```css
.share-tiers {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}
.share-tiers .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 16px;
}
.share-tiers .diagram-arrow {
  text-align: center;
  font-size: 20px;
  line-height: 1;
}
```

</Diagram>

## Roles on a share grant {#roles}

When you share with a specific user or org, you pick a role:

- **Viewer** — read only.
- **Commenter** — read + add comments, but cannot edit the resource or manage shares.
- **Editor** — read + write.
- **Admin** — read + write + manage shares (can add/remove other people).

`admin` does NOT change ownership — there's still exactly one owner per resource, distinct from the share grants.

### Group grants {#group-grants}

Some resources opt in to group sharing. A group must belong to the resource's
organization, and each current group member gets the selected share role on
that resource. See [Organizations, Teams & Permissions → User groups](/docs/organizations-teams-permissions#user-groups)
for what groups do and do not scope.

## What's covered {#covered}

Every template that stores user-authored work uses this model. Concretely:

- **Content** — documents
- **Slides** — decks
- **Design** — designs and assets
- **Clips** — screen recordings (Loom-style)
- **Plan** — visual plans and PR recaps
- **Forms** — form definitions
- **Calendar** — booking links
- **Analytics** — dashboards, saved analyses, and tokenized session replays
- **Assets** — asset libraries
- **Brain** — sources, knowledge entries, proposals, and projects
- **Extensions** — sandboxed mini-apps (see [Extensions](/docs/extensions#sharing))

Every one of these uses the same `ownableColumns()` schema helper, the same `share-resource` action, and the same `<ShareButton>` UI. Move from one template to another and the share dialog looks identical.

<Callout id="doc-block-sharing1" tone="info">

Calendar **events** are not part of this system — they're fetched live from the Google Calendar API and have no `owner_email` row of their own, so access to an event follows Google Calendar's own sharing and invite model. Only booking links (the public scheduling pages you create) are framework-shareable resources.

</Callout>

## What's not covered {#not-covered}

A few areas are intentionally outside the sharing system:

- **Personal-data apps** such as Mail — user-scoped by design. There's no "share my inbox" concept.
- **External source-of-truth apps** — access control lives in the upstream system, not the agent-native app.
- **Anonymous public URLs** — form publish slugs and booking-link slugs that expose a URL to logged-out users are a separate axis. They live alongside the sharing system, not on top of it.

## The share UI {#share-ui}

Every shareable resource gets a share button in its header. Clicking it opens a popover anchored to the button (not a modal) with:

- "Add people or teams" autocomplete — search users in the org or paste an email, plus a role picker.
- A Google Docs-style `Notify people` checkbox for individual email grants.
- A list of current grants with role pickers and a remove control.
- General access: visibility selector (`Private` / `Organization` / `Public link`).
- A copy-link button that respects the current visibility.

Moving a resource from `Private` to `Organization` asks for confirmation first. Nothing changes until the user confirms. Cancel or Escape keeps the current access. If saving fails, the dialog shows the error and the user can try again. Other visibility changes save at once. The confirmation lives in the share UI only. `set-resource-visibility` is unchanged, so the agent and any other caller still change visibility directly and the resource's sharing policy still decides what is allowed.

<WireframeBlock id="doc-block-sharing2">
  <Screen
    surface="popover"
    caption="Share popover — anchored to the header button, not a modal"
    html={
      "<div style='display:flex;flex-direction:column;gap:16px;padding:18px 20px;min-width:360px;box-sizing:border-box'><div style='display:flex;align-items:stretch;gap:8px'><input placeholder='Add people or teams' style='flex:1'/><span class='wf-pill'>Editor</span></div><label style='display:flex;align-items:center;gap:8px' class='wf-muted'><input type='checkbox' checked/> Notify people</label><div style='display:flex;flex-direction:column;gap:10px'><strong>People with access</strong><div style='display:flex;align-items:center;gap:10px'><span class='wf-pill'>AL</span><span style='flex:1'>Ada Lovelace (you)</span><span class='wf-muted'>Owner</span></div><div style='display:flex;align-items:center;gap:10px'><span class='wf-pill'>MT</span><span style='flex:1'>marketing@company.com</span><span class='wf-pill'>Editor</span><span class='wf-muted'>Remove</span></div></div><div style='display:flex;flex-direction:column;gap:8px;padding-top:14px;border-top:1.4px solid var(--wf-line)'><strong>General access</strong><div style='display:flex;align-items:center;gap:10px'><span class='wf-pill accent'>Organization</span><span class='wf-muted'>Anyone at the org can view</span></div></div><div style='display:flex;gap:8px'><input readonly value='https://app.example.com/share/8fq2k1' style='flex:1'/><button class='primary'>Copy link</button></div></div>"
    }
  />
</WireframeBlock>

The share button is a single import:

```tsx
import { ShareButton } from "@agent-native/toolkit/app/sharing";
<ShareButton
  resourceType="deck"
  resourceId={deck.id}
  resourceTitle={deck.title}
/>;
```

For lists, drop a `<VisibilityBadge visibility={row.visibility} />` next to each row so users can see at a glance what's private vs. shared.

## Shared chats {#shared-chats}

Agent chat threads use the same model. A new or forked chat is private to its owner, and sharing a chat follows the rules above.

Each chat in a history list carries two fields so a row can say whose chat it is and why you can see it:

| Field         | Value                                                                                                             |
| ------------- | ----------------------------------------------------------------------------------------------------------------- |
| `ownerEmail`  | The chat's owner.                                                                                                 |
| `accessBasis` | `owner`, `direct` (a grant to you by name), or `organization`. `direct` wins over `organization` when both apply. |

History lists and search take an `ownership` filter:

- `all` (default) lists every chat you can access.
- `mine` lists chats you own, even when they are shared with the organization.
- `shared` lists chats you can access that someone else owns.

The server applies `ownership` before pagination, so `limit`, `offset`, and `hasMore` describe the filtered list. Search takes the same filter. An unknown value is rejected with a 400 instead of falling back to `all`. Access rules do not change, and listing never loads transcripts.

Migration: custom history list and search callers must send a positive integer `limit` and a non-negative integer `offset`, or omit them to use the defaults. Malformed values now return HTTP 400 instead of being coerced. Valid limits above 200 are capped at 200.

In client code, `useChatThreadList()` from `@agent-native/core/client` pages one list at a time:

```tsx
import { useChatThreadList } from "@agent-native/core/client";

const shared = useChatThreadList({ ownership: "shared", pageSize: 50 });
// shared.threads, shared.hasMore, shared.loadMore(), shared.error
```

`refreshOnThreadUpdates` defaults to `true`. Set it to `false` to skip refreshes triggered by `notifyChatThreadsUpdated()`. Initial loading, manual `refresh()`, paging, and reloading after an Activity resumes still work. Changing this option preserves loaded rows and does not fetch by itself.

A failed request sets `error` and keeps the rows already loaded. It never looks like an empty list. `<ShareButton>` and `<ShareDialog>` refresh mounted chat lists after they save. If you change a chat's sharing some other way, call `notifyChatThreadsUpdated()` so mounted lists refetch.

For a ready-made sidebar that combines owned chats, shared chats, and eligible started drafts, import `useOwnershipChatHistory()` and `OwnershipChatHistoryRail` from `@agent-native/toolkit/app/chat/ownership-history`. The adapter keeps owner and access-basis details on shared rows, pages each section independently, and retains rows and local optimistic changes when a refresh fails. Its mutation callbacks return `Promise<boolean>`, allowing the app to navigate only after a write succeeds. See [Sharing Kit](/docs/toolkit-sharing#shared-chat-history) for an example.

An active empty draft remains selectable and runnable while its first save is pending. Until persistence is confirmed, hide pin, rename, and archive actions, and the adapter rejects calls to those callbacks. `ownDraftRows` supplies persistence provenance for owned drafts, while confirmed owned rows from the server list keep their normal actions. For custom `OwnDraftRow` values, set optional `persisted?: boolean` to `true` only for a confirmed persisted server row owned by the current user. Do not infer persistence from message count or active ID. Role, privacy, and whether the active draft is empty remain separate facts.
Pins are personal: `pinnedAt` is the caller's pin, including on shared chats; list and search sort by it. `useChatThreads().pinThread(id, pinned)` keeps its existing signature. Both `useChatThreads()` and `useChatThreadList()` provide `hideThread(id, hidden)` for shared chats. Hidden chats are excluded from list and search unless `includeHidden: true` is set; their rows and single-thread reads include the caller's `hiddenAt`. Opening a hidden chat by id or link still works. Owners cannot hide their own chats; they archive instead. Rename and archive remain owner-only. Hide throws `ChatThreadMutationError` on HTTP refusals (`status` 403 for owners, 404 without access); successful calls return `true`. The HTTP contract is `POST /_agent-native/agent-chat/threads/:id/hide` with `{ hidden: boolean }`; list/search use `includeHidden=1`. The `chat-history` tool supports `pin`, `unpin`, `hide`, `unhide`, and search with `includeHidden: true`. Revoking access hides prior viewer state; deleting a thread deletes that state.

## Same model, agent and UI {#agent-and-ui}

The framework auto-mounts these actions in every template — the agent calls them as tools, and the UI calls them through `useActionQuery` / `useActionMutation`:

| Action                       | What it does                                                                                   |
| ---------------------------- | ---------------------------------------------------------------------------------------------- |
| `share-resource`             | Grant a user or org access at a specific role. Optional `notify` controls email notifications. |
| `unshare-resource`           | Revoke access for a user or org.                                                               |
| `list-resource-shares`       | Show current visibility plus all explicit grants.                                              |
| `set-resource-visibility`    | Change to `private`, `org`, or `public`.                                                       |
| `create-agent-resource-link` | Mint a temporary read-only `agent_access` URL for a registered agent-readable resource.        |

Tell the agent "share this design with the marketing team as editors" and it calls `share-resource` against the same endpoint the UI uses. The result shows up in the share dialog the next render.

## Agent-readable share links {#agent-readable-links}

Human share links should also work for agents when the resource is a document-like artifact: plans, documents, decks, designs, analytics dashboards, saved analyses, clips, and similar "things you would send to a person." Public links expose a small SSR discovery script that points at structured JSON. Private resources use a short-lived `agent_access` token scoped to exactly one resource.

The default flow is:

1. Register the resource with `agentReadable`.
2. Add a read-only JSON endpoint that returns the same content a viewer can see.
3. Add an SSR discovery script on the human page with `renderAgentReadableResourceDiscoveryScript()`.
4. Let agents call `create-agent-resource-link` when they need a temporary private URL.

```ts
registerShareableResource({
  type: "deck",
  resourceTable: schema.decks,
  sharesTable: schema.deckShares,
  displayName: "Deck",
  titleColumn: "title",
  getResourcePath: (deck) => `/p/${deck.id}`,
  agentReadable: {
    resourceKind: "slides:deck",
    getContextPath: () => "/api/deck-agent-context.json",
    getPagePath: (deck) => `/p/${deck.id}`,
  },
  getDb,
});
```

The context endpoint must be read-only. It should return data only when the row is `public` or `verifyScopedAgentAccessToken()` accepts the token for that exact `resourceKind` and `resourceId`. Set `Cache-Control: no-store`, `Referrer-Policy: no-referrer`, and avoid including write credentials, provider blob URLs, or secrets. Token links should not change the resource visibility.

## Building it into a new template {#building}

If you're creating a template (see [Creating Templates](/docs/creating-templates)), wiring sharing in is short. Two additions to your schema:

```ts
import {
  table,
  text,
  ownableColumns,
  createSharesTable,
} from "@agent-native/core/db/schema";

export const decks = table("decks", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  data: text("data").notNull(),
  ...ownableColumns(), // adds owner_email, org_id, visibility
});

export const deckShares = createSharesTable("deck_shares");
```

<DataModel
  id="doc-block-1wo5mhr"
  title="Resource + companion shares table"
  summary={
    "Coarse visibility lives on the resource; each fine-grained grant is a row in the shares table."
  }
  entities={[
    {
      id: "deck",
      name: "decks",
      note: "...ownableColumns()",
      fields: [
        {
          name: "id",
          type: "text",
          pk: true,
        },
        {
          name: "title",
          type: "text",
          nullable: false,
        },
        {
          name: "owner_email",
          type: "text",
          nullable: false,
          note: "The single source of truth for ownership.",
        },
        {
          name: "org_id",
          type: "text",
          nullable: true,
        },
        {
          name: "visibility",
          type: "enum",
          nullable: false,
          note: "private | org | public",
        },
      ],
    },
    {
      id: "deckShare",
      name: "deck_shares",
      note: "createSharesTable() — one row per grant",
      fields: [
        {
          name: "id",
          type: "text",
          pk: true,
        },
        {
          name: "resource_id",
          type: "text",
          fk: "decks.id",
          nullable: false,
        },
        {
          name: "principal_type",
          type: "enum",
          note: "user | org",
        },
        {
          name: "principal_id",
          type: "text",
          note: "email (user) or org id (org)",
        },
        {
          name: "role",
          type: "enum",
          note: "viewer | commenter | editor | admin",
        },
        {
          name: "created_by",
          type: "text",
        },
        {
          name: "created_at",
          type: "text",
        },
      ],
    },
  ]}
  relations={[
    {
      from: "deckShare",
      to: "deck",
      kind: "n-n",
      label: "grants access to",
    },
  ]}
/>

One registration call in `server/db/index.ts`:

```ts filename="server/db/index.ts"
import { registerShareableResource } from "@agent-native/core/sharing";

registerShareableResource({
  type: "deck",
  resourceTable: schema.decks,
  sharesTable: schema.deckShares,
  displayName: "Deck",
  titleColumn: "title",
  getResourcePath: (deck) => `/deck/${deck.id}`,
  getDb,
});
```

After that, list/read queries pass through `accessFilter()` and write actions use `assertAccess()` to enforce roles.

### Optional hardening flags {#hardening-flags}

`registerShareableResource` accepts two security flags for resources that execute code or carry elevated trust:

```ts
registerShareableResource({
  type: "extension",
  resourceTable: schema.extensions,
  sharesTable: schema.extensionShares,
  // ...
  allowPublic: false, // Reject set-resource-visibility → "public"
  requireOrgMemberForUserShares: true, // Reject user grants to non-org emails
});
```

`allowPublic: false` prevents any caller — agent or UI — from setting the resource's visibility to `public`. `requireOrgMemberForUserShares: true` rejects individual user grants to email addresses outside the resource owner's org. Extensions set both: an extension's HTML runs inside an iframe that calls actions and DB as the _viewer_, so public access would be arbitrary code with the viewer's credentials.

<Callout id="doc-block-1ehh444" tone="risk">

For resources that execute code or carry elevated trust (like extensions), set `allowPublic: false` and `requireOrgMemberForUserShares: true`. Otherwise a public share becomes arbitrary code running with the _viewer's_ credentials.

</Callout>

`getResourcePath` gives notification emails a direct fallback link when a share is created by the agent or another non-UI caller. The full pattern (including create-action ownership stamping and the migration recipe for existing tables) lives in the `sharing` agent skill — the agent reads it on demand when building a sharing-aware feature.

## Security guarantees {#security}

Sharing rides on the framework's broader data-scoping model — list/read/write access to ownable tables goes through `accessFilter()` / `resolveAccess()` / `assertAccess()`, and `org_id`-tagged resources are invisible across orgs. See [Security → Data Scoping](/docs/security#data-scoping) for the full pipeline, the CI guard, and the threat surface.

## What's next {#see-also}

- [**Security — Data Scoping**](/docs/security#data-scoping) — the access-filter and ownership model that sharing rides on
- [**Authentication**](/docs/authentication) — sessions, organizations, and how identity flows into the request context
- [**Extensions**](/docs/extensions#sharing) — sharing in the sandboxed mini-app surface
- [**Creating Templates**](/docs/creating-templates) — wiring `ownableColumns` into a new template's schema
- [**Toolkit piece: Sharing Kit**](/docs/toolkit-sharing) — the share button, share dialog, and visibility badge built on this model
