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

# Cross-app shares

> Read another app's content — a user's blog posts, their photos — with their permission.

Apps can offer a fixed, read-only slice of their content to other apps. Each slice is a **share**, named `share:<provider>.<feature>`: `share:blog.posts` is the blog app's list of published posts. Your app lists the shares it wants in `cskn.json`, asks the user, and reads them with the SDK.

```tsx theme={null}
import { useCrossApp } from '@cskn/sdk';

function LatestPosts({ username }: { username: string }) {
  const posts = useCrossApp('blog', 'posts', { username, limit: 3 });
  if (posts.items.length === 0) return null;

  return (
    <ul>
      {posts.items.map(post => (
        <li key={post.id}><a href={post.url ?? undefined}>{post.title}</a></li>
      ))}
    </ul>
  );
}
```

## Whose permission counts

Every share has a **subject**: whose content it returns. The subject decides who has to say yes.

| Subject | Returns | Who grants it |
| - | - | - |
| `self` | The viewer's own content | The viewer, through your app |
| `user` | One person's content, named by `username` | That person. The viewer needs nothing, and doesn't have to be signed in. |
| `grantors` | Content from everyone who granted the share to your app | Each of those people. The viewer needs nothing. |

The platform applies the owner's visibility before anything comes back. If someone shows an app to followers only, a visitor who doesn't follow them gets nothing. Blocked people and suspended accounts are left out too. What a share returns is safe to show as it is.

## Declaring what you want

List the shares in the `crossApp` field of your app's `cskn.json`. Each deploy publishes the list:

```json theme={null}
{
  "crossApp": ["share:blog.posts", "share:photos.recent"]
}
```

A share that isn't listed can't be asked for or read, even if a user granted it to your app before. Listing one is only a request. The user's grant is what lets you read it.

The deploy warns you about a share its provider doesn't offer, or one that is limited to some apps. It still publishes the list, because the provider may add the share later.

<Note>
  `cskn.json` is the only source. If you remove a share from `crossApp`, the next deploy stops your app from reading it.
</Note>

## Reading

`useCrossApp(provider, feature, options)` reads a share page by page. It is a TanStack Query `useInfiniteQuery`.

<ParamField path="provider" type="string" required>
  The app that offers the share, for example `blog`.
</ParamField>

<ParamField path="feature" type="string" required>
  The share's name within that app, for example `posts`.
</ParamField>

<ParamField path="options.username" type="string">
  Whose content to read. Required for a `user` share.
</ParamField>

<ParamField path="options.limit" type="number">
  Page size. The platform clamps it: 20 by default, at most 50, and a share can set lower bounds.
</ParamField>

<ParamField path="options.enabled" type="boolean" default="true">
  Set to `false` to hold the request.
</ParamField>

<ResponseField name="items" type="T[]">
  Every page loaded so far, flattened. Typed per share; see [Typing a share](#typing-a-share).
</ResponseField>

<ResponseField name="refusal" type="CrossAppError | null">
  The platform's reason for refusing, or `null`. A refusal is not retried.
</ResponseField>

<ResponseField name="fetchNextPage / hasNextPage" type="function / boolean">
  From `useInfiniteQuery`. Load more while `hasNextPage` is `true`.
</ResponseField>

A refusal is the normal case on most profiles: the owner hasn't shared, or shows the app to some people only. If you show a share on someone's user page, draw nothing when it is refused rather than an error.

Without React, use `readCrossApp(provider, feature, { username, limit, cursor })`. It resolves to `{ items, nextCursor }`. Pass `nextCursor` back as `cursor` for the next page. It is `null` on the last page.

### Signed-out visitors

A `user` share can be read with no session, so a visitor to a [user page](/concepts/surfaces) sees the owner's content. The SDK names your app on the request. The platform then returns only what the owner shows to everyone. A `self` or `grantors` share needs a signed-in user.

## Asking for a share

`useCrossAppAccess(scopes)` returns what the user said to each share and a `request` function that opens the consent screen.

```tsx theme={null}
import { useCrossAppAccess, crossAppText } from '@cskn/sdk';

function ShowMyPosts() {
  const access = useCrossAppAccess('share:blog.posts');
  const share = access.shares[0];

  return (
    <label>
      <input
        type="checkbox"
        checked={access.granted}
        disabled={!share || access.requesting}
        onChange={e => (e.target.checked ? access.request() : access.revoke())}
      />
      Show {share ? crossAppText(share.description) : 'my posts'} here
    </label>
  );
}
```

<ParamField path="scopes" type="string | string[]">
  The shares this screen is about. Leave it out to cover every share in your `cskn.json`.
</ParamField>

<ResponseField name="granted" type="boolean">
  `true` when every share in scope is granted. `false` while loading.
</ResponseField>

<ResponseField name="shares" type="CrossAppAccessShare[]">
  One entry per share: `scope`, `subject`, `description` (`{ en, tr }`), `granted`, `providerName`, and `providerActivated` (whether the user has the provider app turned on).
</ResponseField>

<ResponseField name="missing" type="string[]">
  The shares in scope the user hasn't granted.
</ResponseField>

<ResponseField name="request" type="(only?: string[]) => Promise<CrossAppRequestResult>">
  Opens the consent screen for the missing shares, or for `only`. Call it from a tap or a click.
</ResponseField>

<ResponseField name="revoke" type="(only?: string[]) => Promise<{ changed: boolean }>">
  Gives back the granted shares in scope, or `only`.
</ResponseField>

`request` never rejects. It resolves to `{ granted, declined, error? }`:

* `granted` lists the shares that are granted now, including ones granted before.
* `declined` lists the rest.
* `error` is set only when the user wasn't asked.

The user saying no is not an error. Shares granted before aren't asked again. After the screen closes, every cross-app query refreshes.

Ask for what the screen in front of the user needs, not for everything in your `cskn.json`. For a `self` share, asking is how you get to read it. For `user` and `grantors` shares, asking is how the viewer shares their *own* content with your app.

### Where the consent screen appears

| Where your app runs | What happens |
| - | - |
| Mobile, desktop | The shell shows its own consent sheet. It answers after the grant is saved and your app holds a token that carries it, so your next read already works. |
| Browser, app page | A popup with the cskn consent screen. If the browser blocks it, the page goes there and comes back to the same URL. `redirected` is then `true`. Read the access again after the reload. |
| User page, TV, older app versions | No consent screen: `error: 'not_available'`. Grants are stored on the account, so a grant given on a phone holds on the TV too. |

Without React, use `requestCrossApp(scopes)` and `getCrossAppAccess()`.

| `error` | Meaning |
| - | - |
| `not_available` | This surface can't ask. See the table above. |
| `no_gesture` | Called without a tap or click, or while your app wasn't on screen. |
| `busy` | A consent screen is already open. |
| `not_activated` | The user has turned your app off. |
| `invalid` | A scope isn't of the form `share:<provider>.<feature>`. |
| `popup_closed` | Browser only: the user closed the popup. |
| `failed` / `timeout` | The grant couldn't be read or saved. Try again later. |

## Giving a share back

`revokeCrossApp(scopes)`, or `revoke` from the hook, stops your app from holding a share for the signed-in user. Use it for an in-app "stop showing this" switch. It can only narrow your app's access. Granting always goes through the consent screen.

Users can also take shares back themselves. On mobile and desktop they do it under **Profile → App access**. Your app sees that as `granted: false` the next time it reads the access.

## Writing

A share can offer **actions** as well as reads: adding, editing or deleting in the provider app. An action runs with the share's own permission and the same checks as a read. The consent screen tells the user when a share can change their data.

```tsx theme={null}
import { crossAppAction, useCrossAppAction } from '@cskn/sdk';

await crossAppAction('forms', 'collect', 'submit', {
  username: owner,
  body: { form: form.id, values, _csrf: form.csrfToken },
});

// or, as a TanStack Query mutation that refreshes cross-app queries on success:
const submit = useCrossAppAction('forms', 'collect', 'submit');
submit.mutate({ username: owner, body: { form: form.id, values, _csrf: form.csrfToken } });
```

<ParamField path="options.method" type="'POST' | 'PUT' | 'PATCH' | 'DELETE'" default="'POST'">
  The method the action declares. A different method is refused with `METHOD_NOT_ALLOWED`.
</ParamField>

<ParamField path="options.username" type="string">
  Whose data, for a `user` share.
</ParamField>

<ParamField path="options.body" type="object">
  The action's input. The provider validates it.
</ParamField>

`getCrossAppCatalog(provider)` lists each share's `actions` with `name`, `method` and `signedOut`. Most actions need a signed-in user. An action that is `signedOut` on a `user` share can also be called by visitors with no session, for example answering a published form.

### Acting in another app

Some `self` shares open a set of the provider's own API routes instead of offering actions. `share:feed.posting`, for example, lets your app post, react and comment in Feed as the user. Once the user grants the share, call those routes with `api`, the same way the provider's own app does:

```ts theme={null}
import { api } from '@cskn/sdk';

await api.post('/apps/feed', { content: 'Hello' });
await api.post(`/apps/feed/${postId}/react`, { emoji: '❤️' });
```

Each route applies exactly the rules it applies to the provider's own app. A route the share doesn't list answers `403` with `WRONG_APP`, even if the user granted the share. `getCrossAppCatalog(provider)` lists each share's `routes`. These shares work only while the user has the provider app turned on.

| Share | Opens |
| - | - |
| `share:feed.posting` | Posting, deleting, reacting, commenting, reporting, blocking in Feed |
| `share:photos.posting` | Uploading and deleting photos, likes, comments |
| `share:vibes.posting` | Uploading and deleting vibes, likes, comments |
| `share:stories.posting` | Posting and deleting stories, viewing, seeing who watched |
| `share:chat.messaging` | A chat token, finding people, opening a direct conversation |

<Note>
  Reaching another app's data — reading or writing — always goes through `crossApp`. The `permissions` field of `cskn.json` is only for your app's own API and for integrations with other systems, never for another app.
</Note>

## Shares you can use

<AccordionGroup>
  <Accordion title="share:forms.forms — self">
    The viewer's own forms, newest first, for picking one to embed. Typed as `CrossForm`: `id`, `name`, `isPublished`, `acceptingResponses`, `fieldCount`.
  </Accordion>

  <Accordion title="share:forms.collect — user, with a submit action">
    One of the owner's published forms. Read it with `query: { form: id }`; the page has at most one item, typed as `CrossPublicForm`: `fields`, `settings`, `acceptingResponses`, and the `csrfToken` to send back.

    The `submit` action (`POST`, allowed signed out) stores an answer: `body: { form, values, _csrf }`. It applies the same rate limit, validation and spam protection as the form's own page. A rejected field comes back as `400` with `fields: [{ id, code, message }]`. A closed form answers `FORM_CLOSED`.
  </Accordion>

  <Accordion title="share:blog.posts — user">
    One person's published posts, newest first. Drafts never appear. Default page 5, at most 20.

    | Field | Type | |
    | - | - | - |
    | `id` | `string` | |
    | `title` | `string` | |
    | `excerpt` | `string` | Empty when the post has none. |
    | `coverImage` | `string \| null` | |
    | `category` | `string` | |
    | `readTime` | `number` | Minutes. |
    | `createdAt` | `string` | ISO time. |
    | `url` | `string \| null` | The post on its owner's blog. |
  </Accordion>

  <Accordion title="share:photos.recent — user">
    One person's latest public photos. A private photo never appears, not even to its owner. Default page 12, at most 30.

    | Field | Type | |
    | - | - | - |
    | `id` | `string` | |
    | `caption` | `string` | Empty when the photo has none. |
    | `thumbnail` | `string` | Square, grid quality. |
    | `image` | `string` | Native aspect ratio. |
    | `width` / `height` | `number` | Original dimensions. |
    | `createdAt` | `string` | ISO time. |
    | `url` | `string \| null` | The photo on its owner's page. |

    <Warning>
      Image URLs are signed and expire. Show them, but never store them.
    </Warning>
  </Accordion>

  <Accordion title="share:feed.posts — grantors">
    Posts from everyone who shares them with your app, newest first. Original posts only: no reposts or quotes. Typed as `CrossFeedPost`: `id`, `text`, `images` (`{ url, width, height }[]`), `author` (`{ username, name, picture }`), `createdAt`, `url`.
  </Accordion>

  <Accordion title="share:photos.shared — grantors">
    Photos from everyone who shares them with your app, newest first. Private photos appear only to their owner. Same fields as `photos.recent`, plus `author`.
  </Accordion>

  <Accordion title="share:vibes.shared — grantors">
    Clips from everyone who shares them with your app, newest first. Only clips that have finished processing. Typed as `CrossVibe`: `id`, `caption`, `video` (HLS), `thumbnail`, `durationMs`, `width`, `height`, `author`, `createdAt`, `url`.
  </Accordion>

  <Accordion title="share:stories.active — grantors">
    Stories from the last 24 hours of everyone who shares them with your app, newest first. A story its owner shows to followers only reaches followers. Typed as `CrossStory`: `id`, `type`, `status`, `media`, `thumbnail`, `caption`, `durationMs`, `width`, `height`, `author`, `createdAt`, `expiresAt`.
  </Accordion>
</AccordionGroup>

Media URLs from photos, vibes and stories are signed and expire. Show them, but never store them.

`getCrossAppCatalog(provider)` lists what a provider offers: each share's `feature`, `subject` and `description`, and `restricted` when only some apps may ask for it.

## Typing a share

`readCrossApp` and `useCrossApp` type `items` from the `CrossShareMap` interface. The shares above are already in it (`CrossBlogPost`, `CrossPhoto`, `CrossFeedPost`, `CrossVibe`, `CrossStory`). For any other share, add its type yourself:

```ts theme={null}
declare module '@cskn/sdk' {
  interface CrossShareMap {
    'recipes.latest': { id: string; title: string; url: string | null };
  }
}
```

A share that isn't in the map reads as `unknown`.

## Refusals

`crossAppError(error)` returns `{ code, status, message }` for a refused read, or `null` for anything else, such as a network failure.

| `code` | Meaning |
| - | - |
| `SCOPE_NOT_GRANTED` | A `self` share the viewer hasn't granted. Ask with `request`. |
| `SHARE_NOT_GRANTED_BY_OWNER` | That person hasn't shared this with your app. |
| `NOT_VISIBLE` | They show the provider app to some people only, and the viewer isn't one of them. |
| `USER_NOT_FOUND` | No such person, or a block between them and the viewer. |
| `SHARE_NOT_DECLARED` | Your `cskn.json` doesn't list this share. |
| `CONSUMER_NOT_ALLOWED` | The provider only lets some apps ask for this share. |
| `SHARE_NOT_FOUND` | No such share. |
| `USERNAME_REQUIRED` | A `user` share was read without `username`. |
| `APP_TOKEN_REQUIRED` | A `self` or `grantors` share, or an action that isn't `signedOut`, was used signed out. |
| `ACTION_NOT_FOUND` | No such action on this share. |
| `METHOD_NOT_ALLOWED` | The action declares a different method. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.