social components inlay.at
atproto components sdui
86
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 227 lines 9.7 kB view raw view rendered
1# Inlay 2 3[Inlay](https://inlay.at/) lets you build social UIs from components that live on the AT Protocol. 4 5For example, here's a Bluesky post rendered as an Inlay component: 6 7```jsx 8<mov.danabra.Post uri="at://did:plc:ragtjsm2j2vknwkz3zp4oxrd/app.bsky.feed.post/3lkqvm" /> 9``` 10 11The host resolves `mov.danabra.Post` and expands its template into a tree of smaller components: 12 13```jsx 14<org.atsui.Stack> 15 <org.atsui.Row align="center"> 16 <mov.danabra.AviHandle uri="did:plc:ragtjsm2j2vknwkz3zp4oxrd" /> 17 <org.atsui.Caption> 18 <org.atsui.Timestamp value="2026-02-17T02:11:13.240Z" /> 19 </org.atsui.Caption> 20 </org.atsui.Row> 21 <org.atsui.Link uri="at://..."> 22 <org.atsui.Text>hey check this out</org.atsui.Text> 23 </org.atsui.Link> 24 <at.inlay.Maybe> 25 <mov.danabra.PostEmbed uri="at://..." /> 26 </at.inlay.Maybe> 27</org.atsui.Stack> 28``` 29 30Then `AviHandle` and `PostEmbed` expand further, and so on, until everything bottoms out in `org.atsui.*` primitives that the host renders directly into HTML, React, SwiftUI, or whatever it wants. 31 32An Inlay component like `Post` or `AviHandle` is just a name ([NSID](https://atproto.com/specs/nsid)) — anyone can publish an implementation for it, and a *host* (i.e. a browser for Inlay) picks which implementation to use, potentially taking both developer and user preferences into account. 33 34## Packages 35 36| Package | Description | 37|---------|-------------| 38| [`@inlay/core`](packages/@inlay/core) | Element trees, JSX runtime, and serialization | 39| [`@inlay/render`](packages/@inlay/render) | Server-side component resolution | 40| [`@inlay/cache`](packages/@inlay/cache) | Cache policy for XRPC component handlers | 41 42## How it works 43 44Each component is an [`at.inlay.component`](https://pdsls.dev/at://did:plc:mdg3w2kpadcyxy33pizokzf3/com.atproto.lexicon.schema/at.inlay.component#schema) record with one of three body kinds: 45 46- **No body** — a primitive. The host renders it directly. 47- **Template** — a stored element tree with Binding placeholders that get filled in with props. 48- **External** — an XRPC endpoint that receives props and returns an element tree. 49 50Each component also imports an ordered list of DIDs. When the host needs to resolve a child NSID, it walks the import list and looks up `at://{did}/at.inlay.component/{nsid}` — first match wins. 51 52[Atsui (`org.atsui`)](https://pdsls.dev/at://did:plc:e4fjueijznwqm2yxvt7q4mba/com.atproto.lexicon.schema) is the first design system for Inlay — including `<Text>`, `<Avatar>`, `<List>`, and others. In a sense, it's like Inlay's HTML. Atsui is not a traditional component library because it solely defines *lexicons* (i.e. interfaces). Each host (an Inlay browser like [inlay.at](https://inlay.at/)) may choose which components are built-in, and how they work. A host could even decide to not implement Atsui, and instead to implement another set of primitives. 53 54A host chooses its own tech stack. For example, you could write a host [with Hono and htmx](./proto), or [with React Server Components](./app), or even with SwiftUI and a custom server-side JSON endpoint. The resolution algorithm is shared, but each host may interpret the final tree as it wishes. 55 56## Declaring components 57 58There is no visual editor yet. Components are created by writing records to your PDS. 59 60### Pick an NSID 61 62An NSID describes *what* a component does, not *how*. If someone already defined one that fits (e.g. `com.pfrazee.BskyPost` with a `uri` prop), you can implement it yourself. Multiple implementations of the same NSID can coexist — the importer's DID list controls which one gets used. 63 64If you're creating a new NSID, pick one under a domain you control so you can [publish a Lexicon](https://atproto.com/specs/lexicon#lexicon-publication-and-resolution) for it later. 65 66### Component record 67 68This is the record you write to your PDS. 69 70#### Template 71 72The element tree lives inside the record. Bindings are placeholders that get filled in with props at render time. 73 74```json 75{ 76 "$type": "at.inlay.component", 77 "body": { 78 "$type": "at.inlay.component#bodyTemplate", 79 "node": { 80 "$": "$", "type": "org.atsui.Stack", "props": { 81 "gap": "small", 82 "children": [ 83 { 84 "$": "$", 85 "type": "org.atsui.Text", 86 "props": { "children": ["Hello, "] }, 87 "key": "0" 88 }, 89 { 90 "$": "$", 91 "type": "org.atsui.Text", 92 "props": { "children": [ { "$": "$", "type": "at.inlay.Binding", "props": { "path": ["name"] } } ] }, 93 "key": "1" 94 } 95 ] 96 } 97 } 98 }, 99 "imports": [ 100 "did:plc:mdg3w2kpadcyxy33pizokzf3", 101 "did:plc:e4fjueijznwqm2yxvt7q4mba" 102 ] 103} 104``` 105 106The `imports` array lists the DIDs your component needs for resolution — typically the [Atsui DID](https://pdsls.dev/at/did:plc:e4fjueijznwqm2yxvt7q4mba) so you can use [`org.atsui.*` primitives](https://pdsls.dev/at://did:plc:e4fjueijznwqm2yxvt7q4mba/com.atproto.lexicon.schema). The record's rkey is the NSID it implements (e.g. `mov.danabra.Greeting`), so the full URI is `at://{your-did}/at.inlay.component/mov.danabra.Greeting`. 107 108When [rendered](packages/@inlay/render) as `<mov.danabra.Greeting name="world" />`, this component resolves to: 109 110```jsx 111<org.atsui.Stack gap="small"> 112 <org.atsui.Text>Hello</org.atsui.Text> 113 <org.atsui.Text>world</org.atsui.Text> 114</org.atsui.Stack> 115``` 116 117#### External 118 119As an alternative to templates (which are very limited), you can declare components as [XRPC](https://atproto.com/guides/glossary#xrpc) server endpoints. Then the Inlay host will hit your endpoint to resolve the component tree. 120 121Here's a [real external component record](https://pdsls.dev/at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/at.inlay.component/mov.danabra.Greeting) for `mov.danabra.Greeting`. Notice its `body` points to a service `did` instead of an inline `node`: 122 123```json 124{ 125 "$type": "at.inlay.component", 126 "body": { 127 "$type": "at.inlay.component#bodyExternal", 128 "did": "did:web:gaearon--019c954e8a7877ea8d8d19feb1d4bbbb.web.val.run" 129 }, 130 "imports": [ 131 "did:plc:mdg3w2kpadcyxy33pizokzf3", 132 "did:plc:e4fjueijznwqm2yxvt7q4mba" 133 ] 134} 135``` 136 137The `did` is a `did:web`, so the host resolves it by fetching [`/.well-known/did.json`](https://gaearon--019c954e8a7877ea8d8d19feb1d4bbbb.web.val.run/.well-known/did.json) to find the service endpoint, then POSTs props to `/xrpc/mov.danabra.Greeting` and gets back `{ node, cache }`. 138 139This particular handler runs on [Val Town](https://www.val.town/x/gaearon/greeting-jsx/code/main.tsx): 140 141```tsx 142/* @jsxImportSource npm:@inlay/core@0.0.13 */ 143import { component, serve } from "https://esm.town/v/gaearon/inlay/main.ts"; 144import { Stack, Text } from "https://lex.val.run/org.atsui.*.ts"; 145 146component("mov.danabra.Greeting", ({ name }) => ( 147 <Stack gap="small"> 148 <Text>Hello,</Text> 149 <Text>{name || "stranger"}</Text> 150 </Stack> 151)); 152 153export default serve; 154``` 155 156It's recommended to tag XRPC return values as cacheable; see [`@inlay/cache`](packages/@inlay/cache) for how to do this. In the calling code, it's recommended to wrap XRPC components into `<at.inlay.Loading fallback=...>` so that they don't block the entire page. 157 158### Lexicon (optional) 159 160A [Lexicon](https://atproto.com/specs/lexicon) defines the prop schema for an NSID. You don't need one to get started, but publishing one enables type checking, validation, and codegen with [`@atproto/lex`](https://www.npmjs.com/package/@atproto/lex): 161 162```json 163{ 164 "lexicon": 1, 165 "id": "mov.danabra.Greeting", 166 "defs": { 167 "main": { 168 "type": "procedure", 169 "input": { 170 "encoding": "application/json", 171 "schema": { 172 "type": "object", 173 "required": ["name"], 174 "properties": { 175 "name": { "type": "string", "maxLength": 100 } 176 } 177 } 178 }, 179 "output": { 180 "encoding": "application/json", 181 "schema": { "type": "ref", "ref": "at.inlay.defs#response" } 182 } 183 } 184 } 185} 186``` 187 188See [`@inlay/core`](packages/@inlay/core) for JSX support and more ways to build element trees. 189 190## Rendering as a host 191 192A host walks an element tree, resolves each component through its import stack (DIDs), and maps the resulting primitives to output (HTML, React, etc). 193 194[`@inlay/render`](packages/@inlay/render) does the resolution step — call `render()` on an element, get back the expanded tree or a primitive. The host calls it in a loop until everything is resolved. The [render README](packages/@inlay/render) has a minimal working example that turns Inlay elements into HTML. 195 196If you're writing XRPC component handlers, [`@inlay/cache`](packages/@inlay/cache) lets you declare cache lifetime and invalidation tags (`cacheLife()`, `cacheTagRecord()`, `cacheTagLink()`). The host uses these to know when to re-render. 197 198[`proto/`](proto/) is a prototype host built with Hono and htmx. 199 200## Development 201 202```bash 203npm install 204npm run dev # start the Next.js dev server (Turbopack) 205npm run build # production build (Next.js) 206npm run typecheck # type-check 207npm run lint # lint 208``` 209 210## Project structure 211 212``` 213app/ — Next.js app (host UI, routes, pages) 214packages/@inlay/core/ — element primitives and JSX 215packages/@inlay/render/ — rendering engine 216packages/@inlay/cache/ — cache declarations 217db/ — database schema (Drizzle) 218ingester/ — AT Protocol firehose ingester 219invalidator/ — cache invalidation service 220proto/ — prototype host server (Hono) 221lexicons/ — AT Protocol lexicon definitions 222generated/ — generated TypeScript from lexicons 223``` 224 225## License 226 227MIT