# Habitat API This file contains all documentation content in a single document following the llmstxt.org standard. ## Organizational Data Server Habitat's core development is the Organizational Data Server (ODS). Every community or organization on Habitat has an (ODS) which houses all the data for that entity and provides the necessary APIs from which products can interact with the organization. An Organizational Data Server is much like a Personal Data Server in AT Protocol, but with the 'organization' distinction. For a single organization, the Organizational Data Server makes it so that all organization member repositories are hosted on (conceptually) the same server, so member data and identities persist beyond the lifetime that a member may be in the organization. One way to think about this is that an 'organization' is an entity separate and larger than any particular member or set of members, and all data must be tied to this organization entity, rather than to users. Every Habitat organization has admins, which can manage members of the organization and other members. Admins also set metadata about the organization, like the name, description, and which login methods users join with. A more in-depth write-up about why Habitat has developed the Organizational Data Server and the affordances it gives in contrast to the Personal Data Server can be found [here](https://habitat.leaflet.pub/3mo3v5xzvtc2y). ### Administration An administrator of an Organizational Data Server is whoever has root access to the running server. The administrator can set additional controls on the server, such as whether it can be multi-tenant (host more than one Habitat organization) or not. --- ## Overview Habitat is building a data ownership platform for organizations and communities. We are developing an open protocol that defines an Organizational Data Server (ODS), which can be implemented by anyone and for which we offer both managed hosting and self-hosting instructions. The following is a high-level technical architecture diagram of Habitat's Organizational Data Server and how it interacts with application servers built by developers. This is roughly analogous to a Personal Data Server (PDS) / AppViews in AT Protocol, with a few key departures that are explored in the following pages. ``` +--------------------------------------------------------------------+ | | | Organizational Data Server | | | | +---------------------------+ +-------------------------+ | | |+-------++-------++-------+| | | | | || User || User || User || | | | | || Repo 1|| Repo 2|| Repo 3|| | | | | |+-------++-------++-------+| | | | | | | | | | | |+-------------------------+| | | | | || Authentication || | | | | |+-------------------------+| | | | | |+-------------------------+| | | | | || Permissions || | | | | |+-------------------------+| | | | | |+-------------------------+| | | | | || Sync Server || | | | | |+-------------------------+| | | | | | | | | | | | A Habitat Organization | | | . . . | | +-------------|-------------+ +-------------------------+ | +--------------|-|---------------------------------------------------+ | | | |Event Stream | | +--------------------------+ | Application Server | +--------------------------+ ``` --- ## User repositories An Organizational Data Server in Habitat houses user repositories for every member of the organization. This allows all data writes to be tied to a particular identity, and granular permissions between those identities in the concept of an organization. User repositories are much like the AT Protocol concept of repositories, however, they do not support public data at this time and hence do not implement the requirements of the `#atproto_PDS` as of the time of this writing. For documentation reference on the AT Protocol concept of repositories, see [here](https://atproto.com/specs/repository). --- ## Agents ### Machine-readable documentation Point an agent at these directly: - [`llms.txt`](pathname:///llms.txt) — an index of this documentation. - [`llms-full.txt`](pathname:///llms-full.txt) — the full prose in one file. - [`openapi.json`](pathname:///openapi.json) — the OpenAPI specification for every XRPC endpoint. - Any single page as markdown — append `.md` to its URL, e.g. `https://api.habitat.network/building/permissions.md`. ### Working with coding agents If you build an app that reads or writes data stored in Habitat, using a coding agent — Claude Code, Cursor, or similar — give it this context up front. Copy the block below into your project's `CLAUDE.md`, `AGENTS.md`, or equivalent. ### Template ````md # Building on Habitat This project uses Habitat, a data ownership layer for organizations built on AT Protocol primitives. ## Key facts - Habitat's hosted instance is `https://pear.habitat.network`. XRPC endpoints live under `/xrpc/`, e.g. `https://pear.habitat.network/xrpc/network.habitat.space.getRecord`. - Authentication is standard AT Protocol OAuth. The app OAuths with Habitat; Habitat brokers to user's identity provider. - For use-cases where users bring their own AT Protocol account, Habitat acts as the [identity resolver](https://atproto.com/specs/did) (see also the [atproto identity guide](https://atproto.com/guides/identity)). It rewrites the `#atproto_pds` entry in a resolved DID document to point at itself, so standard atproto clients route through it without Habitat-specific code. - Data is AT Protocol records and blobs, stored in per-identity repositories and addressed by collection NSID plus record key. - A space groups records across repositories and is the unit that permissions are granted against. ## Which APIs to use For permissioned data, use only these namespaces: - `network.habitat.space.*` — records, blobs, and repo operations within a space. This is the primary surface. - `community.opensocial.*` — community membership, invites, profiles, and spaces. - `network.habitat.opensocial.createOrg` — creating an organization. ## Documentation - Index for agents: https://api.habitat.network/llms.txt - Full prose: https://api.habitat.network/llms-full.txt - OpenAPI spec: https://api.habitat.network/openapi.json - HTTP reference: https://api.habitat.network/docs/api ## Conventions - Never invent lexicon names. Every valid NSID appears in the OpenAPI spec; check there before writing a record, and prefer the namespaces listed above. - Use the regular atproto API client (e.g. `@atproto/api`) rather than hand-rolled fetch calls. - To use the Habitat Space Proxy until atproto spaces are part of reference PDS'es, use the npm package `@habitat-network/habitat` which provides an identity resolver to route space requests through Habitat. - **Do not use anything that references cliques**. Cliques are a deprecated permissioning primitive. ```` --- ## Identities & Authentication ## Identities #### Decentralized Identifiers (DIDs) Identities in Habitat are based on DIDs. Both organizations and members of an organization have a unique identity, defined by a DID document. The `did:web:` DID method is used for all identities in Habitat. Application developers should reference both organizations and users by DID. Currently, the DID docs for both organizations and users only have a single service in the services field, a `#habitat` service which points to the ODS (Organizational Data Server) on which the identity lives. Because Habitat is starting with the permissioned data usecase, the `#atproto_PDS` service would be an inaccurate service to include, though eventually we will reach parity with that and switch over. #### Organization identities Every organization in Habitat has its own identity. This is a `did:web:` that directly references the Habitat domain the organization is hosted on. For organizations that use the managed hosting provided by Habitat's main instance, this looks like `.id.habitat.network`. For self-hosted instances, this is `.`. #### User identities Habitat uses **DIDs (Decentralized Identifiers)** as the foundation of user identity. Every user is identified by their DID, and has a handle that is tied to the organization. The username that is in this handle is chosen by the user when they are added as a member of the organization. So, for example, `alice.acmecorp.id.habitat.network` is the AT Protocol handle for a user Alice who is a part of the Acmecorp organization hosted by Habitat. (These handles will be tied to organization-given domains and shortened in the future). --- ## Authentication #### Login methods When creating a Habitat organization, admins will be prompted about which login method to set for the entire organization. The options offered today are sign-in with password, sign-in with AT Protocol, and sign-in with Google. Choosing a login method is a final operation and cannot be changed in the future. We currently do not support multiple login methods for an organization, though we may in the future. Every login method creates a DID for the user within Habitat's Organizational Data Server, but the AT Protocol and Google login methods defer to those services for authentication. #### OAuth Habitat's Organizational Data Server runs its own OAuth 2.0 server, which is what your app authenticates with on behalf of both users and on behalf of the application itself. You can use a standard OAuth client that supports PKCE to authenticate with Habitat's OAuth server. The OAuth flow will redirect the user to a Habitat consent screen before navigating back to your page once the auth flow has completed. See an example of how we do it here. #### Admin-approved applications Applications that are built on Habitat must get organization approval in order to receive an OAuth credential for the org's identity, which allows the app to read and write to the Habitat Data Server. Applications must implement a flow that redirects to Habitat to request relevant permissions, and from which authenticated admins can either approve or deny the request. This flow is currently a work-in-progress and updated documentation is coming soon. #### Service Auth The Habitat Data Server also supports [service proxying](https://atproto.com/specs/xrpc#service-proxying) for authenticated inter-service requests. --- ## Data Model Habitat uses the [same data model as AT Protocol](https://atproto.com/specs/data-model). Refer to the linked documentation for the most up to date documentation. At a high level, every piece of data stored in Habitat is tied to an identity, and stored in that identity's [repository](../arch/repositories.mdx). Data can be in the form of either **records** or **blobs**. Records are JSON records that adhere to the AT Protocol Lexicon schema language. [Blobs](https://atproto.com/specs/blob) can be any file format and must be referenced by at least one record via the `blob` lexicon field type, otherwise they get garbage collected. Blobs are content addressed, and records have unique record keys referencing them. --- ## Introduction Habitat provides a data ownership layer for organizations. This means that building a product or application with Habitat looks a bit different than the traditional product stack today. The components that Habitat provides as primitives for teams to use are [authentication & identities](auth.mdx), [fine-grained permissioning](permissions.mdx), and [syncing primitives](sync.mdx). Using these foundations, products built on Habitat push to and pull from an [Organizational Data Server](../arch/overview.mdx) to deliver richer user experiences. Architecturally, Habitat is an adaptation of AT Protocol that directly solves for community and organizational needs. Products and applications build on Habitat are roughly analogous to AppViews in AT Protocol. We highly recommend reading and getting familiar with a few basic concepts of AT Protocol, such as user repositories, DIDs, lexicons, AppViews, and how sync works. Though we provide high-level documentation of relevant components, more in-depth documentation and linked readngs of these concepts can be found on the official AT Protocol docs [here](https://atproto.com). --- ## Permissions User repositories within a Habitat organization only support permissioned data. To do so, we use the proposed Permissioned Spaces implementation developed by the AT Protocol team. The APIs for this are documented here, and in our HTTP reference [here](../api/habitat-api.info.mdx). ### Spaces Permissioned Spaces are the current proposal for implementing AT Protocol data that can only be read by a scoped set of users. This proposal has not become a part of the official AT Protocol spec yet, and details about how the proposed implementation works can be found in the Permissioned Data Diaries [here](https://dholms.leaflet.pub). Habitat implements spaces according to the outlined API in the diaries and in [this PR](https://github.com/bluesky-social/atproto/compare/main...permissioned-data). The proposed API seems close enough to what will become the finalized one that we have gone ahead and implemented it with the plan to be forwards-compatible with the finalized version and migrate to that implementation at that point. The rest of this page gives an overview about how permissioned data works in spaces and any assumptions and departures we have made from the proposed spec. #### Overview Currently, all data on AT Protocol is fully public. Of course, this excludes many applications and products from being built on the protocol. Because Habitat is building for communities and organizations where privacy is key, we have started from private data rather than public. User data in Habitat does not support public data. Instead, all writes must go through our Spaces API, which makes records private by default. #### Spaces in Habitat Records are always [put](../api/network-habitat-space-put-record) directly into spaces with the `network.habitat.space.putRecord` XRPC endpoint. A space must first be created with the `network.habitat.space.createSpace` XRPC endpoint before a record can be put into it. Spaces which have a member list that can be altered with the `network.habitat.space.addMember`, `network.habitat.space.removeMember` APIs. Whenever a space is created with `network.habitat.space.createSpace`, the space owner is the organization (the org's DID) that the authenticated user making the request belongs to. The user becomes an admin of the space. The space host is Habitat, more specifically our orgnaizational data server. Because all spaces are owned by the organization, an OAuth credential for the org's DID is valid for applications to read permissioned data (see [Authentication](auth.mdx#oauth)). #### Roles, groups, and inheritance Habitat's Data Server will provide additional APIs for application developers to declare roles, groups, and inheritance relationships on specific subjects using those primitives. These will likely expose APIs that model off of user-relation-objects tuples in OpenFGA and encode these tuples as records owned by an organization. This would allow concepts such as teams within an organization to be consistent across applications, rather than redefined by each application. This is currently a work-in-progress and developing in conversation with other teams in AT Protocol building community and group infrastructure. --- ## Sync ⚠️ Under construction ⚠️ Habitat follows the standards AT Protocol has set in terms of data synchronization mechanisms for organizational data. However, rather than applications sync-ing with individual user data repositories to get updates, Habitat's Organizational Data Server provides a subscription primitive for syncing an entire organization's updates (documentation coming soon). We also are developing Sap, a standalone binary / Go library that is modelled off of AT Protocol's [Tap tool](https://atproto.com/blog/introducing-tap), and handles backfill and recovery for desynchronized states. Sap is meant to be run by application servers building on top of Habitat, embedded into their architecture. --- ## For communities **community** *n.* A gathering of people around some shared purpose. #### Why use Habitat? Today, many communities use a variety of tools and products to communicate and do things together. Often, groups use some amalgamation of Slack, Google Drive, Eventbrite, Luma, Signal, and other products. However, many of these tools are built for specific customers, often enterprises, and their features and pricing reflects those priorities. Habitat provides infrastructure for communities to build applications that are attuned to their unique needs and can enable communities to diminish and eliminate their reliance on Big Tech platforms. Habitat can be deployed in a variety of ways depending on what works best for a given community: from fully managed hosting to managed hosting deployed into a cloud provider of choice to fully self-hosted. --- ## For developers #### Why build on Habitat? Habitat provides core infrastructure that is replicated across any product built today, such as user identities, authentication, permissions, and data syncing. We aim to get these primitives right so developers can focus on what matters most to users: the product experience. Additionall, because Habitat is built around concepts of data ownership and interoperability, products build on Habitat can naturally integrate and interoperate with each other, delivering product experiences that are much harder to build on the web today. In short, the reasons to build on Habitat are: - skip rebuilding auth, permissions, etc. - no cold start from the data perspective - give your customers more usability out of their data If you are interested in building on Habitat and have questions about the best way to get started, we'd love to chat. Reach out here. --- ## For organizations #### Why use Habitat? Today, organizational data is fragmented across SaaS tools that restrict access, create high switching costs, and limit cross-application workflows. This fragmentation is a core bottleneck to workflows and agentic systems that require a deep, unified context. Habitat's customer-controlled data platform lets enterprises own their organizational data, so agents can access full context and custom internal software can be built on a persistent data layer, shared by agents and humans. Habitat sits as a foundational layer—enabling a new category of AI-native, cross-application workflows and bespoke enterprise software. #### Get started Habitat is in early-stages, and we are actively developing user-facing products built on our platform. However, the platform itself is in alpha and ready to use today. Some ways organizations can get started using Habitat right away are: - develop internal tools that have unified permissions and identities across them - build a structured context layer that exists outside of any particular model or provider - preserve and back-up data tied to identities, even as people come and go from your organization to build historical context --- ## Self-hosting guide ⚠️ Habitat is under active development. Releases are cut from `main` every week. ⚠️ Habitat's Organizational Data Server, Pear, ships as a Docker image (`ghcr.io/habitat-network/pear`, for `linux/amd64` and `linux/arm64`). ### Releases Every Tuesday, the newest commit on `main` that passed CI is published as a [GitHub release](https://github.com/habitat-network/habitat/releases) named `vYYYY.M.D-`. Each release has a matching image tag (without the `v`) and a `docker-compose.yml` pinned to that image. The `latest` image tag points at the newest release. Older `v0.0.x-testing-N` tags are from a previous architecture and do not work with this guide. ### Quick start You need a Linux server with Docker Compose, a domain pointed at it, and a reverse proxy that terminates TLS. ```bash mkdir pear && cd pear curl -LO https://github.com/habitat-network/habitat/releases/latest/download/docker-compose.yml cat > .env < EOF docker compose up -d curl http://localhost:8000/health # -> ok ``` Then point your reverse proxy at `localhost:8000`, e.g. with Caddy: ``` pear.example.com { reverse_proxy localhost:8000 } ``` By default, Pear stores its data in SQLite on a Docker volume. It generates its encryption keys and signing key on first boot and saves them to the same volume. ### Postgres To use Postgres instead of SQLite, set `HABITAT_DB=postgres://user:password@host:5432/dbname` in `.env`. Pear always connects with `client_encoding=UTF8`, but creating the database with `ENCODING 'UTF8'` is recommended. Behind PgBouncer or another transaction-mode pooler, add `default_query_exec_mode=simple_protocol` to the connection string. ### Full reference For all configuration options, upgrades, backups, and building from source, see the [Pear self-hosting README](https://github.com/habitat-network/habitat/tree/main/build/debian/pear). The copy at a given release tag matches that release. --- ## What is Habitat? Habitat is building an open-source and self-hostable data ownership layer for organizations. Our mission is to make data ownership part of the internet's infrastructure. Habitat gives communities and organizations a data server with identities, user data repositories, fine-grained permissioning, and syncing mechanisms built-in, and Habitat is made with open protocols and standards. That way, groups can focus on building and using the applications and experiences that serve their needs, without giving up control of their data, now or at any point in the future. In the organizational setting, many resources are sunk into data migrations as a result of switching service providers, and building bespoke knowledge bases to pull data out from SaaS services. By making organizational data ownership the default, Habitat aims to increase the usability of internal data and reduce the time spent on data management. #### What can I do with the Habitat? If you are: - someone creating custom or niche applications for a community you are a part of - a developer seeking to give your users more value from your product through [data interoperability](https://joinreboot.org/p/rhizome) - an AT protocol builder hoping to serve organizations and groups beyond the big-world and global network use cases then Habitat enables you to pursue these goals by providing the backend primitives and frameworks that underpin communitiy and organizational data and allow real applications to be built on top, without needing to build them from scratch. #### For AT Protocol developers: Habitat is an adaptation of [AT Protocol](https://atproto.com) built specifically for the organizational and community use case. At Habitat, we believe that the primitives afforded by AT Protocol--data ownership, data interoperability, credible exit--are just as important to collectives as they are to individuals. Beyond using the internet to connect with each other socially, we also use the internet to work, collaborate, and organize towards shared goals and ambitions. While AT protocol provides better affordances at an individual level, for groups with governance and entities that exist beyond any particular set of users, additional considerations come into play: data and identities that persist as people come and go, sub-groups and roles that permeate across application boundaries, and complicated permissions with rich inheritance structures. Habitat extends AT Protocol and reconfigures some of its main components to deliver on these goals. See our Architecture docs for more information. --- ## Habitat API Welcome to the Habitat API reference! We are currently under development and our API may make breaking changes without notice. Stay tuned [@habitat.network](https://bsky.app/profile/habitat.network) on Bluesky and [habitat.leaflet.pub](https://habitat.leaflet.pub) for future formal releases. ### Endpoints At the moment, all endpoints are served over HTTPS at: ``` https://pear.habitat.network/xrpc/ ``` ### Authentication Habitat authenticates apps with the standard AT Protocol OAuth flow — not static bearer tokens. Your app OAuths with Habitat, and Habitat in turn acts as an OAuth client against the user's own PDS. Because a Habitat-resolved DID document names Habitat as the PDS, a standard atproto OAuth client discovers Habitat's `/.well-known/oauth-protected-resource` and authorizes against it with no Habitat-specific code. See [Getting started](/docs/space-proxy/getting-started) for a working client, and [Identities & authentication](/docs/building/auth) for how identities, login methods, and service auth fit together. [View the full API reference →](/docs/api/habitat-api) --- ## Introduction(Opensocial) ### Habitat organizations Habitat organizations manage a set of members with roles and host AT Protocol spaces for those members. The organization model is a partial implementation of the [opensocial proposal](https://github.com/bluesky-social/proposals). This way, the organization can remain in control of which members and apps have access to which parts of an organization's data. Even though a member's contributions to an organization are written to the member's own repo, the organization controls the ACL of that data. When an organization is created at [https://home.habitat.network/opensocial](https://home.habitat.network/opensocial), it mints a new AT Protocol identity for the organization. Habitat-compatible apps can use a member's credential to interact with the organization's data. ### Membership and roles Organization accounts are seeded with 2 spaces and some records that encode details about the organization: - `community.opensocial.about/self` - publicly visible space - `community.opensocial.profile/self` - record with name, description, and avatar. - `community.opensocial.members/self` - space only visible to members - `community.opensocial.membership/` - record that indicates that a user is part of the organization ### Creating spaces Members of an org can create spaces by invoking `community.opensocial.createSpace` on the organization's PDS endpoint using service auth. Based on the `role` param included in the request, a `community.opensocial.access/self` record is seeded into the new space. That record lets the space host know which members can be granted a space credential for fetching contents of the space. Creating a space also writes a `community.opensocial.space` record into the `community.opensocial.members/self` space. This helps with discovery of the space. ### Authorizing apps The spaces protocol uses client attestation to let space hosts authorize specific clients. Organizations require client apps to be authenticated by an org admin before they can fetch any space in the organization. An app must initiate an OAuth flow against the organization's identity where an admin will sign in and approve the app along with its requested scopes for the entire organization. Once this flow is complete, a `community.opensocial.appAccess/` record is written into the space. When a client app requests a space credential, it includes a client attestation and the space host verifies that the client ID has been granted access to those scopes in the organization. Completing the OAuth flow for the organization's identity also gives the client app an access token that it can use to write the organization's repo within a space. This is useful for managing shared organization data that doesn't fit in an individual member's repo. --- ## simplespace compatibility Applications that are only compatible with simplespaces will still work with ReBAC spaces. The `network.habitat.simplespace.*` methods are a narrow view onto the same relation graph the `network.habitat.relationship.*` methods write to. ### How the methods map | Simplespace method | What it does in the graph | | ------------------ | --------------------------------------------------------------------------------------------------------- | | `createSpace` | Grants the creator [`owner`](./overview.mdx#roles) on the new space, as a [`userRelation`](./records.mdx#userrelation) record. | | `addMember` | Grants the new member `writer` | | `removeMember` | Revokes the DID's grant on the space, whichever role it was. | | `listMembers` | The **expanded** set of DIDs holding `reader`, identical to [`resolveRelations`](./endpoints.mdx#listing) with `relation: reader`. | | `deleteSpace` | Deletes the space and every relation governing it. | - **A "member" is anyone with `reader`.** Since `owner ⇒ manager ⇒ writer ⇒ reader`, everyone the graph grants any role to is a member as far as simplespace is concerned. `addMember` grants `writer` because that is what a plain simplespace member is expected to be able to do — write into the space. - **The space owner is a member without a record.** The DID in the space URI holds `owner` implicitly, which is why `removeMember` refuses to remove it. ### Where the two views diverge Simplespace's vocabulary is smaller than the graph's, so reading a ReBAC space through it is lossy: **`listMembers` flattens.** It returns DIDs, so the "reasons" for membership like space relations and role inheritance are not visible. **`removeMember` only removes direct grants.** It deletes the user's `userRelation` on that space. If the user also has a grant on the space indirectly through a `spaceRelation`, they will still remain a member, and `listMembers` will still return them. Removing them for real means removing them from the group, or removing the group's grant with [`deleteRelation`](./endpoints.mdx#writing-relations). **`addMember` cannot express anything but `writer`.** Granting a weaker or stronger role requires the relationship methods. None of these are states a simplespace client can create on its own. A space only ever managed through `addMember`/`removeMember` holds nothing but direct user grants, and the two views stay exactly in step. The divergences only shows up once some other app uses the relationship methods to update the ACL. --- ## Endpoints Every endpoint is served at `https://pear.habitat.network/xrpc/{nsid}`, and each row links to its full request/response schema in the [HTTP reference](/docs/api/habitat-api). ### Authentication Applications can make requests to the space host using service auth tokens from the user's PDS. This lets the space host verify that the user has permission to the spaces they are asking about. With the [space proxy](/docs/space-proxy) you can also use an OAuth token since we centralize the authorization server between the space host and PDS. ### Writing relations Both writes are upserts keyed by the subject, so re-granting a subject a different role [replaces its existing grant](./records.mdx#one-role-per-subject) rather than adding a second one. Each returns the URI of the relation record it wrote. | Endpoint | Required role | Notes | | ---------------------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`relationship.setUserRelation`](/docs/api/network-habitat-relationship-set-user-relation) | `manager` | Grants `relation` to a user DID on `space`. Writes a [`userRelation`](./records.mdx#userrelation) record into the space. `SpaceNotFound` if the space doesn't exist. | | [`relationship.setSpaceRelation`](/docs/api/network-habitat-relationship-set-space-relation) | `manager` | Grants `relation` to the userset "everyone with `subjectRole` on `subject`" on `space`. Writes a [`spaceRelation`](./records.mdx#spacerelation) record. This is how groups, nested groups, and cross-space inheritance are expressed. | | [`relationship.deleteRelation`](/docs/api/network-habitat-relationship-delete-relation) | `manager` | Deletes by relation-record URI — the URI returned by a write or by `listRelations`. Removes both the record and the underlying tuple. `RelationNotFound` if nothing is there. | The role is checked on the space the relation governs: for `deleteRelation`, that's the space embedded in the record URI, not a separate parameter. ### Checking access Checks resolve the whole graph — role implications, group membership, nested groups, and cross-space usersets — and answer with a single `allowed` boolean. Use these rather than reading relation records and resolving them yourself. | Endpoint | Required role | Notes | | ---------------------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`relationship.checkUserRelation`](/docs/api/network-habitat-relationship-check-user-relation) | `reader` | Does this DID hold `relation` on `space`? Resolves through group-spaces and nested usersets. | | [`relationship.checkSpaceRelation`](/docs/api/network-habitat-relationship-check-space-relation) | `reader` | Does the userset "holders of `subjectRole` on `subject`" hold `relation` on `space`? Answers cross-space questions like "are spaceA's writers readers of spaceB?" | ### Listing | Endpoint | Required role | Notes | | ----------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`relationship.listRelations`](/docs/api/network-habitat-relationship-list-relations) | `reader` | The **stored** relations governing a space — the interoperable read surface other apps use to understand the permission structure. Returns `userRelationView` and `spaceRelationView` unions, each with the record URI. Filter by `subjectDid`, `subjectType` (`user`/`space`), or `relation`. Not expanded: it returns grants as written. | | [`relationship.resolveRelations`](/docs/api/network-habitat-relationship-resolve-relations) | `reader` | The **expanded** set of user DIDs holding `relation` on a space, flattening usersets and role implications. | | [`relationship.listRelatedSpaces`](/docs/api/network-habitat-relationship-list-related-spaces) | — | The reverse: spaces on which a given DID holds `relation`, expanded the same way. Optional `type` filter by space NSID. Results are filtered to the spaces the **caller** can read, so no space-level role is required up front. | `listRelations` and `resolveRelations` answer different questions: the first tells you what a manager configured, the second tells you who that actually reaches. A user who is only a member of a group granted access appears in `resolveRelations` but has no `userRelation` record of their own. ### Not yet implemented - **Pagination.** No listing endpoint takes a cursor; each returns one unpaginated page. --- ## Getting started In this example we create a space for a design document, give one teammate access to it directly, and let everyone on the eng team in through their group space. By the end, the doc has an ACL that says *"Alice can write here, and anyone on eng can read it"* — and adding someone to eng later gets them in without touching the document's space. Every call goes to `https://pear.habitat.network/xrpc/{nsid}` and carries a bearer token — an OAuth access token through the [space proxy](/docs/space-proxy), or a service auth token from the user's PDS. The examples below use `$TOKEN` for whichever you have. ### 1. Create a space First, the space the document lives in. Spaces are created with the simplespace method — there is no ReBAC-specific create yet. ```bash curl -X POST https://pear.habitat.network/xrpc/network.habitat.simplespace.createSpace \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "network.habitat.document" }' ``` ```json { "uri": "at://did:web:pear.habitat.network/space/network.habitat.document/3lbf2kqhx7c2n" } ``` The returned URI is the created under the Habitat space host (`did:web:pear.habitat.network`). ### 2. Add a specific user Alice is working on this doc with us, so she gets a grant of her own. [`setUserRelation`](./endpoints.mdx#writing-relations) grants one DID one role. Requires `manager` on the space, which the creator has via `owner`. ```bash curl -X POST https://pear.habitat.network/xrpc/network.habitat.relationship.setUserRelation \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "space": "at://did:web:pear.habitat.network/space/network.habitat.document/3lbf2kqhx7c2n", "subject": "did:plc:alice", "relation": "writer" }' ``` ```json { "uri": "at://did:web:pear.habitat.network/space/network.habitat.document/3lbf2kqhx7c2n/did:web:pear.habitat.network/network.habitat.relationship.userRelation/" } ``` The response URI is the [`userRelation` record](./records.mdx#userrelation) the grant was written as — pass it to `deleteRelation` to revoke. The call is an upsert keyed by the subject, so calling it again with `"relation": "reader"` [replaces the grant](./records.mdx#one-role-per-subject) rather than adding a second one. ### 3. Add another space The rest of the eng team should be able to read the doc, and we don't want to add them one at a time. The team already exists as its own space — a group-space of type `network.habitat.group` — whose members are its `reader`s. Bob and Carol are on it. Now we grant the whole team access to the doc in one call. [`setSpaceRelation`](./endpoints.mdx#writing-relations) grants a role to *everyone holding `subjectRole` on another space* — here, everyone who can read `eng` becomes a reader on the doc. ```bash curl -X POST https://pear.habitat.network/xrpc/network.habitat.relationship.setSpaceRelation \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "space": "at://did:web:pear.habitat.network/space/network.habitat.document/3lbf2kqhx7c2n", "subject": "at://did:web:pear.habitat.network/space/network.habitat.group/eng", "subjectRole": "reader", "relation": "reader" }' ``` Nothing is copied — the doc's ACL holds one record pointing at the group, not a snapshot of who was in it. Adding someone to the `eng` group later gives them `reader` here immediately, and the grant survives as one record no matter how the group changes. ### 4. Query the resolved members Two grants are in place now — Alice on the doc, and the eng team on the doc — so who can actually read it? [`resolveRelations`](./endpoints.mdx#listing) expands the graph — role implications, group membership, and nested groups — into a flat list of DIDs. Requires `reader` on the space. ```bash curl -G https://pear.habitat.network/xrpc/network.habitat.relationship.resolveRelations \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "space=at://did:web:pear.habitat.network/space/network.habitat.document/3lbf2kqhx7c2n" \ --data-urlencode "relation=reader" ``` ```json { "dids": ["did:plc:creator", "did:plc:alice", "did:plc:bob", "did:plc:carol"] } ``` Four DIDs, from three different sources: Alice because her `writer` grant from step 2 implies `reader`, Bob and Carol through their membership in `eng`, and the creator because `owner` implies everything. Nothing in the call had to know which was which. Ask for `relation=writer` instead and only the creator and Alice come back — the eng team was granted `reader`, and roles imply downward, not up. To see the grants that produced this list rather than the people it reaches, call [`listRelations`](./endpoints.mdx#listing) instead. ### 5. List the spaces a member can reach Finally, the question an app asks on load: what should we show the person who just signed in? [`listRelatedSpaces`](./endpoints.mdx#listing) is the reverse query: the spaces on which a DID holds a role, expanded the same way. Results are filtered to spaces the **caller** can read, so no space-level role is needed up front — passing the caller's own DID answers "what can I get to?". ```bash curl -G https://pear.habitat.network/xrpc/network.habitat.relationship.listRelatedSpaces \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "did=" \ --data-urlencode "relation=reader" \ --data-urlencode "type=network.habitat.document" ``` ```json { "spaces": [ "at://did:web:pear.habitat.network/space/network.habitat.document/3lbf2kqhx7c2n" ] } ``` If Bob is the caller, the doc comes back even though he was never granted anything on it — his only path to it is through `eng`, and the expansion follows it for him. The optional `type` filter narrows to one space type, which is how an app lists just the spaces it knows how to render. Next: [Relationship records](./records.mdx) for what these calls write, or [Endpoints](./endpoints.mdx) for the full surface. --- ## ReBAC [simplespaces](https://github.com/bluesky-social/proposals/blob/main/0016-permissioned-data/README.md#required-pds-space-management-simplespace) defines a space management implementation that supports basic access control that all PDSs will support. However, some applications will need more fine grained control over space permissions. While the spaces protocol leaves room for custom space management implementations that applications can specify, the ACLs will be trapped in that application hurting cross-app interoperability and space host migrations. Habitat's relationship lexicons provides a space management implementation based on [ReBAC](https://auth0.com/blog/relationship-based-access-control-rebac/) that is compatible with simplespaces. It represents the ACL as edges (relations) in a graph where nodes are users and other spaces. This lets spaces inherit members from other spaces, enabling a much more powerful permission model. Importantly, it writes the relationships of the ACL into the space itself so other applications and space hosts can parse them. A space host resolves the permission graph encoded in the space before granting a space credential. It also provides XRPC methods that applications can use to query the graph like checking if a user has a role in a space, listing members of a space by role, and even listing all spaces a user has a role on. This makes it easier for applications to enforce permissions on the spaces they have crawled either by querying the space host or building their own index of the graph. ### Roles **Relations are four roles**, in a fixed implication chain: ``` owner ⇒ manager ⇒ writer ⇒ reader ``` Holding a role implies holding every role below it, so an `owner` passes a `reader` check without a second grant. The chain is built into the authorization model, not evaluated in application code. | Role | Can | | --------- | ----------------------------------------------------- | | `owner` | Everything, including deleting the space | | `manager` | Manage the space's relations — grant and revoke roles | | `writer` | Write records into the space | | `reader` | Read records in the space, and read its relations | --- ## Relationship records Every grant is a record in the space it governs. There are two lexicons — one for granting to a user, one for granting to another space's role. Both are written into their space under the space authority's repo, so a space's full permission structure is readable by anyone that can read the space. [`listRelations`](./endpoints.mdx#listing) is a helper that will list both types of records for a space. Since the records are written in the space authority's repo, the only way to write them is to use [setUserRelation](./endpoints.mdx#writing-relations) or [setSpaceRelation](./endpoints.mdx#writing-relations). That will authorize the calling user and write to the space auhthority's repo on their behalf. Relation records in a space that are not in the space authority's repo can be ignored. ### userRelation `network.habitat.relationship.userRelation` — grants a role to one user, by DID. | Field | Type | Required | Description | | ----------- | -------- | -------- | ------------------------------------------ | | `subject` | DID | yes | The user the role is granted to. | | `relation` | string | yes | `owner`, `manager`, `writer`, or `reader`. | | `createdAt` | datetime | no | When the grant was written. | ```json // in space at://did:web:space.host/space/network.habitat.document/testdoc { "$type": "network.habitat.relationship.userRelation", "subject": "did:plc:alice", "relation": "writer", "createdAt": "2026-04-02T17:05:00Z" } ``` Reads as: **Alice is a writer on `testdoc`.** ### spaceRelation `network.habitat.relationship.spaceRelation` — grants a role to a everyone holding `subjectRole` on the `subject` space. | Field | Type | Required | Description | | ------------- | --------- | -------- | ---------------------------------------------------------------------- | | `subject` | space URI | yes | The space whose role-holders form the userset. | | `subjectRole` | string | yes | The role held on `subject`: `owner`, `manager`, `writer`, or `reader`. | | `relation` | string | yes | The role granted on the object space. | | `createdAt` | datetime | no | When the grant was written. | ```json // in space at://did:web:space.host/space/network.habitat.document/testdoc { "$type": "network.habitat.relationship.spaceRelation", "subject": "at://did:web:space.host/space/network.habitat.group/eng", "subjectRole": "reader", "relation": "writer", "createdAt": "2026-04-02T17:05:00Z" } ``` Reads as: **everyone who can read the `eng` group is a writer on `testdoc`.** ### One role per subject A grant is keyed by its subject, not by its role: the record key is derived deterministically from the subject (and, for a space relation, the `subjectRole`). Granting a subject a role on a space it already holds a role on **replaces** the old grant rather than adding a second one — there is exactly one `userRelation` per (user, space) and one `spaceRelation` per (subject, subjectRole, space). Since roles imply one another, this loses nothing: an `owner` grant already covers everything a `reader` grant would. --- ## Endpoints(Space-proxy) Every endpoint is served at `https://pear.habitat.network/xrpc/{nsid}`, and each row links to its full request/response schema in the [HTTP reference](/docs/api/habitat-api). Status is relative to the [permissioned data proposal](https://github.com/bluesky-social/proposals/blob/main/0016-permissioned-data/README.md): - 🟢 behaves as the proposal describes - 🟡 deviates from the proposal or ignores some parameters - 🔴 not implemented ### Space management Proposal `com.atproto.simplespace.*`. All of these take OAuth; Habitat does not implement the proposal's `manage` scope operations, so authorization is the space's owner relation instead. | Endpoint | Status | Notes | | ------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------- | | [`network.habitat.simplespace.createSpace`](/docs/api/network-habitat-simplespace-create-space) | 🟡 | The authority is assigned by Habitat rather than taken from the caller; `did` may only name the caller or the caller's org. | | [`network.habitat.simplespace.addMember`](/docs/api/network-habitat-simplespace-add-member) | 🟢 | Owner only. Will include the added user's repos in the space. | | [`network.habitat.simplespace.removeMember`](/docs/api/network-habitat-simplespace-remove-member) | 🟢 | Owner only. | | [`network.habitat.simplespace.listMembers`](/docs/api/network-habitat-simplespace-list-members) | 🟡 | `cursor` is accepted but ignored — one unpaginated page. | | [`network.habitat.simplespace.deleteSpace`](/docs/api/network-habitat-simplespace-delete-space) | 🟢 | Owner only; fans out `notifySpaceDeleted`. | | `network.habitat.simplespace.getSpace` | 🔴 | Spaces carry no host-specific `config` to describe. | | `network.habitat.simplespace.updateSpace` | 🔴 | Spaces carry no host-specific `config` to describe. | ### Records and blobs Proposal `com.atproto.space.*`. Writes take OAuth and only ever target the caller's own repo; reads additionally accept a space credential. | Endpoint | Status | Notes | | ------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`network.habitat.space.putRecord`](/docs/api/network-habitat-space-put-record) | 🟡 | According to the proposal, users will technically be able to put records into any space. It's the space host's responsibility to not propogate repos that don't belong to the space during `listRepos`. Since Habitat is the repo and space host, we just block writes to repos the user doesn't have access to. `validate` returns `NotSupported` — records are not checked against their lexicon. | | [`network.habitat.space.getRecord`](/docs/api/network-habitat-space-get-record) | 🟢 | | | [`network.habitat.space.listRecords`](/docs/api/network-habitat-space-list-records) | 🟡 | `limit`, `cursor`, and `reverse` are accepted but ignored — one unpaginated page. | | [`network.habitat.space.deleteRecord`](/docs/api/network-habitat-space-delete-record) | 🟢 | | | [`network.habitat.space.getBlob`](/docs/api/network-habitat-space-get-blob) | 🟢 | | | `com.atproto.space.createRecord` | 🔴 | Use `putRecord`. | | `com.atproto.space.applyWrites` | 🔴 | There is no batched write; apply each change on its own. | | [`network.habitat.repo.uploadBlob`](/docs/api/network-habitat-repo-upload-blob) | | Habitat extension, with no proposal equivalent. Upload a blob here first, then reference the returned blob in a space record. | ### Sync Served by Habitat in both the space host and repo host roles, since it is both for every space it hosts. Callable with OAuth or a space credential. | Endpoint | Status | Notes | | -------------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`network.habitat.space.listSpaces`](/docs/api/network-habitat-space-list-spaces) | 🟢 | Lists the spaces the caller holds a permissioned repo in — those it has written at least one record to, most recently written first — matching the proposal. A space the caller can only read is not listed. `type`/`did` filters; `limit`/`cursor` accepted but ignored. | | [`network.habitat.space.listRepos`](/docs/api/network-habitat-space-list-repos) | 🟡 | `limit`/`cursor` are accepted but ignored — one unpaginated page. | | [`network.habitat.space.listRepoOps`](/docs/api/network-habitat-space-list-repo-ops) | 🟢 | Takes `since` and `limit` (default 100). The final page carries the signed commit; it is omitted when Habitat holds no signing key for that repo's owner. | | [`network.habitat.space.getLatestCommit`](/docs/api/network-habitat-space-get-latest-commit) | 🟢 | | | [`network.habitat.space.getRepo`](/docs/api/network-habitat-space-get-repo) | 🟢 | Returns the CAR with the signed commit and the DRISL index as its two roots. | ### Notifications | Endpoint | Status | Notes | | ----------------------------------------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------ | | [`network.habitat.space.registerNotify`](/docs/api/network-habitat-space-register-notify) | 🟢 | Space host only, space credential only. Registrations expire after 24 hours and must be renewed. | ### Credentials | Endpoint | Status | Notes | | -------------------------------------------------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- | | [`network.habitat.space.getDelegationToken`](/docs/api/network-habitat-space-get-delegation-token) | 🟡 | OAuth, 60-second tokens. Signed with Habitat's host key (`#habitat`). | | [`network.habitat.space.getSpaceCredential`](/docs/api/network-habitat-space-get-space-credential) | 🟡 | Requires a delegation token. Credentials last 1 hour where the proposal defaults to 2, and `clientAttestation` returns `NotSupported`. | ### OAuth scopes The proposal's `space:` scopes aren't implemented yet. Callers have access to all space types. --- ## Getting started(Space-proxy) The only integration step is **using Habitat as your identity resolver**. Habitat resolves the identity through the public network (or its own directory) and then rewrites the `#atproto_pds` service entry in the returned DID document to point at Habitat. Every downstream piece of the atproto stack — OAuth discovery, XRPC routing — follows that pointer, so your app talks to Habitat without knowing anything special. ### TypeScript ```bash npm install @habitat-network/habitat ``` ```ts import { BrowserOAuthClient } from "@atproto/oauth-client-browser"; import { HabitatIdentityResolver } from "@habitat-network/habitat"; const client = new BrowserOAuthClient({ clientMetadata, identityResolver: new HabitatIdentityResolver(), // defaults to https://pear.habitat.network }); const session = await client.signIn("alice.bsky.social"); ``` The resolver satisfies `@atproto-labs/identity-resolver`'s `IdentityResolver` interface, so it drops into the standard browser OAuth client. Pass a service URL to target a self-hosted instance: `new HabitatIdentityResolver("https://pear.example.com")`. Note that the resolver performs no client-side bidirectional handle/DID verification — it trusts the Habitat instance you point it at. ### Go Use indigo's API directory with Habitat as the host: ```go import "github.com/bluesky-social/indigo/atproto/identity/apidir" dir := apidir.NewAPIDirectory("https://pear.habitat.network") ident, err := dir.LookupHandle(ctx, syntax.Handle("alice.bsky.social")) // ident.PDSEndpoint() == "https://pear.habitat.network" ``` ### Direct XRPC ```bash curl "https://pear.habitat.network/xrpc/com.atproto.identity.resolveIdentity?identifier=alice.bsky.social" ``` ### Authentication Because the resolved DID document names Habitat as the PDS, a standard atproto OAuth client discovers Habitat's `/.well-known/oauth-protected-resource` and authorizes against Habitat. Habitat brokers that flow: it runs its own OAuth server for your app while acting as an OAuth client against the user's real PDS, so the user still authenticates with their own provider. Next: [Endpoints](./endpoints.mdx). --- ## Space Proxy In order to let developers start experimenting with the recently published [ATProto permissioned data proposal](https://github.com/bluesky-social/proposals/blob/main/0016-permissioned-data/README.md), Habitat is offering a PDS wrapper that implements the proposal. When apps sign in to user PDSs through Habitat, standardized PDS endpoints will be forwarded directly to the PDS. Spaces endpoints are handled by us and repos are maintained centrally. Once permissioned data is finalized and implemented, we'll migrate these repos to the real PDSs. Start with [Getting started](./getting-started.mdx) to use Habitat for identity resolution. Habitat overrides the resolved DID's PDS endpoint so that auth and requests work as normal from there. Your app OAuth's with Habitat using the standard atproto OAuth protocol and we OAuth with the user PDS. ### How spaces work Make sure to reference the [permissioned data proposal](https://github.com/bluesky-social/proposals/blob/main/0016-permissioned-data/README.md) for more details. Here's a high-level overview: A **space** is an authorization and sync boundary for a set of permissioned records, identified by an `(authority, type, skey)` triple. Records inside a space are _not_ public and never appear in a user's public repo or on the firehose. - Each member of a space writes to their **own permissioned repo** scoped to that space. A space is the union of its members' repos, not one shared document. - The space **authority** (host) decides who may read the space, enumerates the writer set, and routes write notifications. - A repo's state is summarized by a **signed commit** over an [LtHash](https://engineering.fb.com/2019/03/01/security/homomorphic-hashing/) — an order-independent, incrementally updatable hash — so a syncer can fold in ops and verify the result without replaying history. Signatures are deliberately deniable: the signature covers only random per-commit bytes, and a MAC binds the state digest to them. ### How Habitat's implementation differs Habitat makes some deviations to the proposal to get things working in the proxy arrangment. #### Endpoint NSIDs Once the proposal is finalized, the spaces endpoints are expected to live under the `com.atproto.*` namespace like other PDS endpoints. However, since Habitat is using the WIP spec and also because our semantics are slightly different (as seen below), we're namespacing the new spaces endpoints under the `network.habitat.*` namespace. This will also make migrating to the finalized APIs more clear in your app. #### Simple Spaces The proposal calls for a `simplespace` protocol implemented by all PDSs. Habitat plans on offering our own space management protocol that will be compatible with simple spaces. For now we offer a limited version of the protocol that just implements the member list. We'll bring client attestation, managing app, and public spaces support in upcoming releases. Another key difference is that all spaces created using the Habitat's `simplespace` APIs will be owned by Habitat so that we can manage the ACL ourselves. The creating user still has "owner" level permissions over the space but the space authority will be `did:web:pear.habitat.network`. This shouldn't impact applications except that syncing the space will point to Habitat instead of the space owner. #### Signing Since Habitat doesn't have access to a user's private key, we can't sign commits and token using it. Instead we use the `did:web:pear.habitat.network#habitat` verification method. The intention of the space proxy is to get developers thinking about how to use spaces to organize permissioned data. We suggest skipping verification steps for now since the spaces are centrally managed anyway. However, for those curious, here are the details of Habitat signatures: ##### Commits The signature ctx matches the proposal protocol exactly. The only deviation is which key does the signing. A verifier can just hard code the key as `did:web:pear.habitat.network#habitat` when verifying commit signatures. ##### Tokens The delegation tokens should be signed by the user's private key so that the space authority can verify their access before granting space credentials. Since Habitat is handling `space.getDelegationToken` we just use Habitat's private key to sign the token. The proposal requires `"kid": "#atproto"` in the space delegation JWT. Habitat set `"kid": "#habitat"` instead. Verifiers (which will mostly just be Habitat's `space.getSpaceCredential`) case based on kid to verify the token. The token itself is a compact JWS. Decoded, a delegation token for Alice against a public space looks like: ```json // header { "alg": "ES256K", "kid": "#habitat", "typ": "atproto-space-delegation+jwt" } // claims { "iss": "did:plc:alice", "sub": "at://did:web:pear.habitat.network/space/network.habitat.group/3lartabcd2s22", "aud": "did:web:pear.habitat.network#atproto_space_host", "iat": 1785974400, "exp": 1785974460, "jti": "qX3n8Yh2VjKpLmR7sTuAwQ" } ``` Note that `iss` is still Alice since the token asserts her access. [Endpoints](./endpoints.mdx) lists the full surface method by method along with deviations from the proposal. --- ## Build on Habitat Habitat is an open-source, self-hostable data ownership layer for organizations, built on [AT Protocol](https://atproto.com) primitives: identities, user-owned repositories, fine-grained permissions, and syncing. ### Start here 1. **[What is Habitat?](/docs/habitat)** — what the platform is and who it is for. 2. **[Quickstart](/docs/space-proxy/getting-started)** — point an atproto OAuth client at Habitat as its identity resolver and sign a user in. Runs against our hosted instance; nothing to install. 3. **[Building on Habitat](/docs/building/intro)** — identities and auth, permissions, data, and sync. 4. **[HTTP API reference](/docs/api)** — every XRPC endpoint. 5. **[Self-hosting](/docs/guides/self-hosting)** — run your own instance with Docker. ### For AI agents - [llms.txt](pathname:///llms.txt) — an index of this documentation. - [llms-full.txt](pathname:///llms-full.txt) — the full prose in one file. - [openapi.json](pathname:///openapi.json) — the OpenAPI specification.