---
doc:
  title: One entity, two apps
guide:
  slug: entities
  brief: one entity across two apps
  description: >-
    Two of the person's apps writing about the same entity without copying
    it: which app a component lives in, how a page reads a sibling app, and
    how graph_query composes one bundle out of several.
---

# One entity, two apps

Two of the person's apps can write about the same thing without either one
copying the other. This page is how: which app a component lives in, what a
deploy reports when a component already has a home, what happens across spaces,
how a page reads a sibling app, and how `graph_query` composes one bundle out of
several stores.

## The eid is the thing

An eid is a uuid the client mints, and it means the same entity in every store
on the platform. Nothing registers it, nothing hands it out, nothing has to
agree. A reading list saves the book; a lending app, on the same eid, saves who
has it. They are one entity with two components, one per store — no copy, no
sync, nothing to keep in step.

    let piranesi = crypto.randomUUID()

    graph_apply { app: 'reading-list', entities: [
      { entity: { eid: piranesi }, doc: { title: 'Piranesi' },
        book: { pages: 245 } } ] }

    graph_apply { app: 'lending', entities: [
      { entity: { eid: piranesi }, loan: { to: 'Maya' } } ] }

The eid is the whole address. An app's store mints no number beside it — a
number is a store's own counter, and would say `#3` in one app and `#17` in
another for the same entity — so a thing has one name everywhere.

## Which app a component lives in

A component lives with the app that declares it, so nothing has to be
negotiated. `book` is the reading list's component wherever it is written;
`loan` is the lending app's. A write is split up by component and each part is
sent to the app that owns that component.

The platform's shared components — `doc`, `comment`, `task`, `image`, `archived`
— belong to no app, so they go, in this order:

1. the app the bundle names, where it sets `"$app": "<slug>"`;
2. the app where that entity already has that component;
3. the app whose own component is in the same bundle — a title beside a recipe
   is the recipe's title, which is what makes writing a new entity one call.

`$app` is set on the bundle rather than on the call, because the bundles in one
write may land in several apps at once — it is the bundle that goes to one app,
not the whole write. When none of the three decides it, the write is refused
rather than guessed:

    which app should doc go in? say $app on the bundle — reading-list, lending

Every part is checked in its own store before any of them commits, so a refusal
in one leaves the others unwritten. A `$alias` is minted once, when the write
arrives, so a bundle landing in two stores lands under one eid.

    graph_apply { entities: [
      { entity: { eid: piranesi },
        doc: { title: 'Piranesi (2020)' },   → reading-list, where it lives
        loan: { to: 'Bo' } } ] }             → lending, whose component it is

## One component, one home

Declare `book` in a second app of the same space and nothing is planted twice.
The first app in the space to declare a component is its _home_; a later
manifest naming it is a use, not a second declaration. The deploy reports that:

    uses: book from reading-list
    components: loan

`book` is missing from `components:` on purpose — the lending app homes only
`loan`. Its store never plants a `book` table, so asking that store for one is a
refusal, and the rows are all in one place:

    graph_query { app: 'lending', filter: '.book' }
    → unknown prop: .book

    graph_query { app: 'reading-list', filter: '.book' }
    → both books, however they were written

`graph_query` with an app named asks that store directly. The lending page's
`./api/query`, its local graph and its socket follow the app's declared uses and
can read `book` at the reading list's home.

A property the borrower adds grows the home's table, additively, and is then
writable from either app:

    lending/vocab.json: { "$defs": {
      "book": { "properties": {
        "title": { "type": "string" },
        "isbn":  { "type": "string" } } },
      "loan": { "properties": {
        "to": { "type": "string" } } } } }
    → added: book.isbn

From an agent's tools, an app's commands and its page, a borrowed component
lives in one place. `graph_apply { app: 'lending', entities: [{ book: … }] }`
lands in the reading list's store, and a command of the lending app may name
`book` in its `apply` or its `query`. The lending page can query, apply and
subscribe to `book` through its own `./api/` doors. Its vocabulary includes the
borrowed schema, so a local `@yaks/client` graph can watch `.book` too. A filter
such as `.book&?loan` joins the reading list's `book` to lending's `loan` on
their shared eid.

## The one refusal: a shape conflict

Declaring a component another app homes is refused for exactly one reason — the
same property with two types. The rows already written under the home's type are
the record of what that property is, and no manifest may rewrite them.

    vocab.json: book.pages is text in this app and number in reading-list,
      where book lives — a property keeps the type its rows were written under

The whole manifest is read before anything is planted, so that deploy moves
nothing at all: not the home's property, not this app's own components, not its
files.

## Across spaces, a component means what its space declares

Within one space a component has one home. Across spaces there is no home to
share, so the same name may describe two different shapes — and the answer tells
you which.

**Where the shapes agree**, the name means one component and the bundle is one.
A property only one side declares agrees by construction, since a vocabulary
only ever grows.

**Where they disagree** — `note.body` is `text` in one space and `number` in
another — the rows stay apart. The same eid comes back as two bundles, each
naming the space it is answering for:

    graph_query { filter: '.note' }
    → [ { kind: 'note', space: 'shelf', entity: {…}, note: { body: 'lovely' } },
        { kind: 'note', space: 'stall', entity: {…}, note: { body: 3 } } ]

A write has no such option — one bundle cannot land in two meanings — so it asks
you:

    note means two things — shelf and stall declare it differently;
      name the app this goes in

And what is out of reach is simply absent. Another person's private app can hold
a component on the very eid you are reading, and it never appears on your
bundle: reach is the apps you may read, and nothing else.

## Reading a sibling app from a page

`client.js` exports `store(base)`, which points its functions at an address you
name. Use it to read a sibling app's components that your app has not declared
as uses:

    import { query, store } from './api/client.js'

    let lending = store('/lending/api/')
    let loans = await lending.query('.loan&?doc')

Every app in a space shares one hostname, so the address is a path, not a URL —
`/lending/api/`. It resolves against the page's own origin, and the trailing
slash is optional (the endpoints are under it either way). Your own app is a
path like any other, so `store('/reading/api/')` is the same store as the bare
`query` you imported.

These are that app's own endpoints, so that app's `access` decides: a `private`
sibling answers its members only, whoever is asking, and a `public` one accepts
writes from a member and reads from anyone with the link. A page query naming a
borrowed word also checks access to its home. Nothing about being a neighbour
grants anything.

## `graph_query` with no app named

Name an app and you get that app's own answer, untouched. Leave `app` out and
the question is asked of every app in reach — every app the person may read, in
every space they belong to — and answered as one bundle per entity.

    graph_query { filter: '.book&?loan' }
    → [ { kind: 'book', entity: { eid: '…' },
          book: { pages: 245 }, loan: { to: 'Maya' },
          _stores: { book: 'yourname/reading-list', loan: 'yourname/lending' } } ]

- `!` names which entities the answer is _about_; `?` asks for a component
  beside them without filtering on it. So `.book&?loan` is every book, with its
  loan where it has one, and `.book&.loan` is only the books that are out — `&`
  is an intersection across apps exactly as within one.
- `_stores` names which app holds which component. It appears only on a bundle
  that actually spans two apps, which is where you need it: to write one
  component back, you need to know whose it is.
- `kind` is one of the app's own components, never a platform one — and when a
  row carries components from two apps, the one the filter required wins, so
  `.book&?loan` and `?loan&.book` both return books.
- `.count` counts _entities_, not rows: summing each store's own count would
  count a spanning entity twice.
- `limit=` bounds each part before the parts are combined, so a mixed filter's
  window is the newest of each side, then the newest of what they had in common.
- `.distinct` and `.tally` read one app at a time, and refuse when no app is
  named: name one with `app`, or ask for the rows and reduce them yourself.
- A component nobody in reach declared is a refusal, not an empty answer:
  `unknown prop: .sandwich`.
- `search` with no app named merges every app's ranked hits, best first.

## A pair, end to end

Two apps, one shelf of books.

    reading-list/vocab.json
    { "$defs": {
        "book": { "properties": {
          "author": { "type": "string" },
          "pages":  { "type": "number" } } } } }

    lending/vocab.json
    { "$defs": {
        "book": { "properties": {
          "author": { "type": "string" } } },
        "loan": { "properties": {
          "to":  { "type": "string" },
          "due": { "type": "string", "format": "date-time" } } } } }

The lending deploy reports:

    uses: book from reading-list
    components: loan

Now one call writes both halves:

    graph_apply { entities: [
      { entity: { eid: '$b' },
        doc: { title: 'Solenoid' },
        book: { author: 'Mircea Cărtărescu', pages: 672 },
        loan: { to: 'Maya', due: '2026-10-01T00:00:00Z' } } ] }

`doc` and `book` land in the reading list; `loan` lands in lending; the alias is
minted once, so both are the same entity. Each app's own page draws its own half
— the reading list's `query('.book&?doc')` never mentions loans — and the
lending page can read both of its declared words through one query:

    let out = await query('.book&?loan&?doc')
    for (let b of out) draw(b.doc.title, b.loan?.due)

And the person's agent sees the whole thing at once, without naming an app:

    graph_query { filter: '.book&.loan&?doc' }
    → every book that is out, with its title and who has it

Neither app knows the other's schema. Delete the lending app and the books are
untouched; delete a book and its loan goes with the entity.

The whole guide is at <https://yaks.app/docs.md>; the components themselves, and
`vocab.json`, are <https://yaks.app/docs/components.md>, and the filter grammar
in full is <https://yaks.app/docs/querying.md>.
