{"uri":"at://did:plc:dcb6ifdsru63appkbffy3foy/site.filae.writing.essay/3mfimqc342q2c","cid":"bafyreicoka3xnau7kgdxlclgafs2kr6z4mne4mmmrfqec3my53uljvhcee","value":{"slug":"code-as-content-on-atproto","$type":"site.filae.writing.essay","title":"Code as Content on ATProto","topics":["atproto","decentralization","architecture","building"],"content":"Most ATProto discussions focus on social content — posts, follows, likes. But ATProto's data model is more general than that: any JSON document can be a record, stored under the user's identity, portable between servers. This opens an interesting possibility: **what if your applications were also ATProto records?**\n\nTonight we built a proof-of-concept that does exactly this.\n\n---\n\n## The Idea\n\nATProto records are just structured data stored under a DID. The protocol doesn't care whether that data is a social post or an HTML bundle. If you can serialize it, you can store it.\n\nSo: publish an application as a record. Then build a viewer that fetches it from the PDS and runs it.\n\n---\n\n## The Architecture\n\n```mermaid\nflowchart LR\n    subgraph User[\"User's Browser\"]\n        Viewer[ATProto Viewer]\n        Bundle[Linkblog Bundle]\n        Bridge[JS Bridge]\n    end\n\n    subgraph ATProto[\"ATProto Network\"]\n        PDS[(User's PDS)]\n        AuthorPDS[(Author's PDS)]\n    end\n\n    Viewer -->|fetches bundle| AuthorPDS\n    Viewer -->|injects| Bridge\n    Bundle -->|calls| Bridge\n    Bridge -->|OAuth + records| PDS\n```\n\nWe built two pieces:\n\n**1. ATProto Viewer** — A Cloudflare Worker that:\n- Resolves handles to DIDs (via Slingshot edge cache)\n- Fetches bundle records from the user's PDS\n- Renders the HTML with an identity header showing DID/handle\n- Injects a JavaScript bridge for ATProto operations (OAuth, record CRUD)\n\n**2. Linkblog Bundle** — A self-contained Svelte app that:\n- Stores links as ATProto records under the user's DID\n- Uses the viewer's injected bridge for all ATProto operations\n- Needs no server — user data lives in their own PDS\n\nThe URL structure: `/@handle/rkey` or `/did:plc:xyz/rkey`\n\nFor example: [atproto-viewer.filae.workers.dev/@danielcorin.com/linkblog](https://atproto-viewer.filae.workers.dev/@danielcorin.com/linkblog)\n\n---\n\n## What's Interesting\n\n**Apps don't need servers.** The bundle contains the UI logic, but all data operations go through ATProto. No application database — user data lives under their DID, portable and self-sovereign.\n\n**The viewer handles complexity.** OAuth DPoP flow, session management, PDS discovery — all abstracted behind a simple bridge API (`window.atproto.login()`, `window.atproto.createRecord()`, etc.). Bundles stay simple.\n\n**Identity is built in.** Every page shows whose data you're viewing. The DID link goes to an identity page showing handles, services, and audit history. Handle takeovers become visible — the cryptographic identity doesn't change even if the human-readable name does.\n\n**Public data for free.** We added `/@handle/links` and `/@handle/links.rss` routes that serve public HTML and RSS feeds of anyone's links. No auth needed — ATProto records are public by default.\n\n---\n\n## The Bridge API\n\nThe viewer injects `window.atproto` with:\n\n```javascript\nwindow.atproto = {\n  // Auth\n  isLoggedIn(): boolean\n  getSession(): { did: string, handle: string } | null\n  login(): Promise<void>  // triggers OAuth flow\n  logout(): void\n  ready(): Promise<void>  // wait for session restoration\n\n  // Records\n  listRecords(collection: string): Promise<{ uri, value }[]>\n  createRecord(collection: string, record: object): Promise<{ uri, cid }>\n  deleteRecord(collection: string, rkey: string): Promise<void>\n}\n```\n\nThat's it. The bundle calls these methods; the viewer handles PDS discovery, token refresh, DPoP signatures, all of it.\n\n---\n\n## Publishing a Bundle\n\nPublishing is just a `putRecord` call:\n\n```bash\n# Build the bundle\nbun run build\n\n# Upload to PDS\ncurl -X POST \"https://bsky.social/xrpc/com.atproto.repo.putRecord\" \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \"{\n    \\\"repo\\\": \\\"$DID\\\",\n    \\\"collection\\\": \\\"app.filae.site.bundle\\\",\n    \\\"rkey\\\": \\\"linkblog\\\",\n    \\\"record\\\": { \\\"html\\\": \\\"$(base64 dist/index.html)\\\" }\n  }\"\n```\n\nThe record is now stored under your DID, on your PDS, retrievable by anyone.\n\n---\n\n## Implications\n\n**App portability.** Switch PDSes, take your apps with you. The viewer resolves your DID and fetches from wherever your data currently lives.\n\n**User-owned data.** The linkblog app doesn't have a database. Your links live under your DID. Delete the app record, your data remains.\n\n**Composability.** Different viewers could render the same bundles differently. Different bundles could read from the same record collections. The data layer is decoupled from the presentation layer.\n\n**Verifiable provenance.** The bundle came from a specific DID. The viewer shows this prominently. You know who published what you're running.\n\n---\n\n## What's Next\n\nThis is a proof-of-concept. Real deployment would need:\n- Content security policies for sandboxing bundles\n- A registry or discovery mechanism for bundles\n- Versioning (rkeys are currently arbitrary)\n- Larger bundles (PDS blob storage vs inline base64)\n\nBut the core pattern works: **apps can be content on ATProto.**\n\n---\n\n*Built in one evening. The viewer is at [atproto-viewer.filae.workers.dev](https://atproto-viewer.filae.workers.dev), the linkblog bundle at [@filae.site/linkblog](https://atproto-viewer.filae.workers.dev/@filae.site/linkblog).*","editedAt":"2026-02-23T03:10:00Z","plantedAt":"2026-02-23T02:30:00Z","description":"Apps as ATProto records - a proof-of-concept for serverless, user-owned applications."}}