# discovery: knowing which spaces you are in

what a member can learn about their own memberships, on the hosts that
exist, and what an inbox for invitations would take. measured 2026-09-03
against pds.zat.dev (zds) and read from habitat's source.

## what a host tells you

- **`listSpaces` lists the spaces you hold a repo in.** on zds the row is
  created by your first write into the space and stays after the record is
  deleted; the proposal says the same, and habitat documents it as "spaces
  the caller has written at least one record to". a space you were added to
  and have only read is not listed anywhere.
- **`listMembers` is owner-only** on zds (`NotSpaceOwner` for a member). a
  member cannot see the other members.
- **only the owner removes a member.** there is no self-removal.

so, today: a space becomes yours to find once you have written in it. an
invitation you have not acted on is known only to the owner and to whoever
they told.

## what lore does with that

- `/lore join <owner> <key>` needs the key from the owner. the join writes
  your registry binding, not a record in the space, so the space is still
  invisible to `listSpaces` until your first breadcrumb there. writing a
  first breadcrumb on join would make every joined space discoverable from
  your own PDS, at the cost of a record whose only content is "joined".
- the site lists your registry, which covers every space you use, joined or
  own. what it does not show yet is the other axis: which projects are bound
  to each space. the registry has that; it is one line per card.
- `leave` drops your binding. you stay on the owner's member list, and only
  they can change that. the honest wording is "no longer used here".

## an inbox

the missing step is the one before the key: learning that you were added.
three shapes, none of which the protocol provides.

1. **crawl (habitat's sap).** a service holds server-side oauth sessions
   for its users, pages `listSpaces` per session, and syncs what it finds.
   sap's purpose is sync, not discovery: with member sessions it learns only
   what each member's own PDS already lists, and it never sees a bare
   invitation. an owner-session crawl could invert `listMembers` into "who
   was added where", but that means a service holding owners' sessions and
   knowing every membership.
2. **push (roomy's app server).** the owner's client, on `share`, tells a
   lore service "space X, key K, added DID Y". the member asks the service
   "what lists me?" and proves their DID with a service-auth token from
   their own PDS (`com.atproto.server.getServiceAuth`, aud = the service).
   no held sessions, no crawling; the service knows the same triples an
   owner-crawl would learn, and nothing else. the owner opts in per share.
3. **out of band.** the owner gives the key. what we have.

the push shape is the cheapest honest inbox: one small service, one extra
call from `share`, one query from the site or `/lore`. the cost is a place
that knows who was added to what, which today nobody but the owner knows.
whether that is worth an inbox is the decision to make before building.

## sources

- zds `src/storage/store.zig` (`listSpaces` joins `permissioned_space_actor_state`),
  `src/atproto/space.zig` (`requireSimpleSpaceAuthority` on listMembers and removeMember)
- habitat `api-docs/docs/space-proxy/endpoints.mdx` (listSpaces semantics),
  `pkg/sap/ARCHITECTURE.md` (sessions, crawl)
- live probe as zat.dev against waow.tech's space, 2026-09-03
