···11+---
22+atroot: true
33+template:
44+slug: bobbin
55+title: Introducing Bobbin
66+subtitle: A diskless, API-only AppView for Tangled
77+date: 2026-07-13
88+authors:
99+ - name: Lewis
1010+ email: lewis@tangled.org
1111+ handle: oyster.cafe
1212+draft: true
1313+---
1414+1515+Hello! It is I, Lewis, with my first Tangled post.
1616+At Tangled my job involves a lot of Research & Development, by
1717+which I mean fucking around and finding out. Let me tell you about one
1818+such finding out, which turned into this new Tangled API.
1919+2020+Bobbin is a read-only, API-only
2121+[AppView](https://atproto.com/guides/glossary#app-view)
2222+for Tangled. It serves the read side of the `sh.tangled.*`
2323+[lexicons](https://atproto.com/specs/lexicon)
2424+over [XRPC](https://atproto.com/specs/xrpc), so a
2525+client can render git repositories, issues, pulls,
2626+comments, stars, & follows, without having to ping N
2727+[PDSes](https://atproto.com/guides/glossary#pds-personal-data-server)
2828+for their data. It has no permanent storage. Everything it knows is only
2929+ever in RAM and gets backfilled from upstream every restart.
3030+3131+> I, along with many others, find the term "AppView" to be too
3232+> abstract or perhaps even unfortunately-named. In atproto, datasets
3333+> are distributed across Personal Data Servers (PDSes) that users each
3434+> own, so an AppView is a single API through which one can query
3535+> a dataset, in contrast to all downstream clients each maintaining a
3636+> connection with every PDS. An AppView is one of the many programs in
3737+> use to reduce the strain on PDSes, so that a PDS is relatively cheap and
3838+> resource-efficient to run.
3939+4040+Let me list some problems that I find Tangled's AppView to have, such
4141+that the journey of discovery that led me to Bobbin makes sense:
4242+4343+1. Tangled's AppView has no API; it doubles as a web client via SSR.
4444+2. It is brittle in the sense that it's not able to backfill
4545+[atproto](https://atproto.com) data easily.
4646+3. Its architecture from the ground up is inherently built for the kind
4747+of web program that assumes *it* is the source of truth of data.
4848+4. Because of all of the above, and the fact that there *is* some
4949+private data such as email->user [DID](https://atproto.com/specs/did)
5050+ties, there ought to only be one AppView for Tangled. This means that
5151+latency to Tangled is vaguely a function of distance from the server
5252+in Stockholm.
5353+5. It is hungry for RAM during a RAM crisis.
5454+5555+Flipping to the inverse of all of these items would yield me my ideal
5656+AppView. An API-only, easily-backfillable, easily-distributable,
5757+easy-on-RAM program.
5858+5959+Why "API-only"? Once it's easy to retrieve Tangled data via generic
6060+XRPC API, more programs can be built on top of it; programs such
6161+as alternative frontends, third-party clients, one's own targeted
6262+dashboards, in my case it was to enable a good CLI (though I suppose
6363+that just comes under alternative frontends). The existing Tangled
6464+AppView being its own web client means there's nothing extendable to
6565+build on, unless one is a scraping wizard; one has to figure out their
6666+own way to go grab the Tangled dataset from the world of PDSes in the
6767+wild.
6868+6969+While I was mulling how to go about creating such a program for fun, I
7070+have also been having a crisis about where to store state, after I've
7171+woken up in cold sweats wondering if Tranquil PDS' homegrown embedded
7272+DB could fail in new mystical ways. How could I avoid this next program
7373+having to write any durable, fallible state? Besides, AppViews are
7474+downstream of PDSes, which means the more we try and hold onto state in
7575+an AppView, the more chance there is that it'll diverge from reality.
7676+All it takes to have an AppView be wildly incorrect is missing a single
7777+record, so it is of upmost importance to have an AppView be able to
7878+reconcile what it thinks it knows with what's actually true.
7979+8080+Being the simpleton that I am, I think that there is no better way
8181+to make a program whose only job is to cache and serve records than
8282+to make it never touch disk in the first place. I would want it to
8383+simply backfill all of its data from scratch on every restart. In that
8484+case, reconciliation with reality is just a restart away and happens
8585+automatically! Was that feasible? What if such a backfill took a whole
8686+day every time? That would sure make deployments slow, would it anyhow
8787+be worth it?
8888+8989+My first tests of this concept gave promising results, I
9090+threw together a little server that could in fact backfill
9191+all of Tangled's atproto dataset in around 15 minutes, using
9292+[Hydrant](https://tangled.org/did:plc:6v3ul2ptnqctyxwkz5ti4amn) as its
9393+aggregator. I decided that an acceptable backfill time would be under
9494+5 minutes or less, so that it would be feasible to switch out running
9595+instances on a blue/green deploy within a reasonable warming timeframe.
9696+9797+> Hydrant is an aggregator for arbitrary atproto datasets that works
9898+> by tailing the live [firehose](https://atproto.com/specs/event-stream),
9999+> backfilling every [repo](https://atproto.com/guides/glossary#data-repo)
100100+> on the network by pulling their
101101+> [CARs](https://atproto.com/specs/repository#car-file-serialization),
102102+> and serving it back over a websocket. All Bobbin has to do is open a
103103+> connection, ask for cursor 0, and (re)build its index off the resulting
104104+> barrage.
105105+>
106106+> Massive gratitude for
107107+> [Dawn](https://tangled.org/did:plc:dfl62fgb7wtjj3fcbb72naae) for having
108108+> made Hydrant! Then for hearing my struggles and going and optimizing it
109109+> also for fun!
110110+111111+Once I reached backfill in 5 minutes, I moved the goalpost for fun, that
112112+the acceptable backfill time should now be 90 seconds or less. This is
113113+around the number we are approaching today, depending on how warm the
114114+upstream Hydrant instance is; ranging from 30 seconds in best-case
115115+scenario to 20 minutes in a disaster where Hydrant is also pulling
116116+records from PDSes from scratch.
117117+118118+> Yes, you too can spin these up & order the entirety of Tangled in 20
119119+> minutes or less, or you get a refund. (Of €0.)
120120+121121+There's a little conundrum that I encountered: what to have the
122122+program serve while the backfill is still warming? Do I bother
123123+having the program serve requests while aggregate/linked data is
124124+possibly incorrect? I think not. So do I have a program serving
125125+nothing in the backfill time that is possibly 20 minutes at
126126+worst? I probably could have done that, but instead we connect to
127127+[Slingshot](https://tangled.org/did:plc:c7mc2fn47ihdihul4vjwsuy3/tree/main/slingshot)
128128+for point-lookups at least and immediately get accurate
129129+data in the meantime, even if the program itself hasn't technically seen
130130+that data yet.
131131+132132+> Slingshot is a wonderful edge cache for atproto records
133133+> & identities. Bobbin asks it for single record and identity
134134+> lookups, so that a freshly-born Bobbin can answer individual
135135+> queries accurately before its own index has warmed up. Slingshot
136136+> is part of the [microcosm](https://www.microcosm.blue/) project,
137137+> a bundle of community-run atproto infrastructure. Thanks
138138+> [Fig](https://tangled.org/did:plc:hdhoaan3xa3jiuq4fg4mefid) for making
139139+> it!
140140+141141+The program that was turning into Bobbin was able to hold all of
142142+Tangled's atproto dataset in around 200MB of RAM, which was good, but
143143+some compression led to 100MB. There is likely more I could compress
144144+that is yet to be done. "Wow, such a small amount of data for the entire
145145+social layer of a whole platform!" you remark, but on the contrary
146146+remember that this is the *smallest* the dataset will likely ever be
147147+from today onwards. Space-saving is of the essence.
148148+149149+Bobbin uses Slingshot as a record/identity resolver in real time for
150150+anything it doesn't already hold in RAM or hasn't necessarily received
151151+from Hydrant. I love Fig's work, don't get me wrong, but my lean on
152152+Slingshot instead of just writing the pings myself was a cop-out to save
153153+time; if there's one major architectural change yet to come to Bobbin,
154154+it's inlining that away such that Bobbin never has to ping Slingshot and
155155+can figure out faster ways of getting the same data.
156156+157157+One would be tempted to throw in an embedded instance of Hydrant to
158158+their AppView program itself, and have the serving of a particular
159159+atproto dataset be just a thin wrapper over the Hydrant data; but
160160+I want it to be easy to spin up possibly hundreds of instances of my
161161+program. Having hundreds of Hydrants all pinging PDSes all the time
162162+would be quite wasteful. One Hydrant instance can serve hundreds of
163163+Bobbins without a sweat.
164164+165165+Since we don't have a Hydrant/relay equivalent for Knot-related data,
166166+Bobbin proxies Knot-related requests straight through to a given knot. I
167167+think a knotstream aggregator is in order, perhaps even just a plugin to
168168+Hydrant that has it serving both. I think Bobbin and Knot Mirror should
169169+merge into one service, but that's just my opinion.
170170+171171+With a "stateless" AppView, we don't have to worry about databases and
172172+their woes: migrations, production massaging when things are slightly
173173+off compared to the "true" dataset, et cetera. Any time we want to
174174+change/add functionality, all we have to do is simply restart
175175+Bobbin and the operation is done. Another benefit is that Bobbin lends
176176+itself to "cloud-native deployments" as the corpos say. Tangled's own
177177+flagship instance runs on a CloudFlare worker.
178178+179179+All this being said, I'm not necessarily married to the idea that Bobbin
180180+never touches disk, I think that one cute improvement could be to simply
181181+dump RAM-stored-data contents as a snapshot once every hour and backfill
182182+the difference at startup; but for now I think there's headroom to
183183+continue down this path, considering Bobbin can healthily run with 200MB
184184+given to it at time of writing. Additionally, my original requirement of
185185+being easy on RAM is still fulfilled for now, but the moment it becomes
186186+too heavy is another point at which Bobbin will offload parts of itself
187187+to disk dynamically during runtime. How do I define "too heavy", you
188188+ask? I look inside my heart, which tells me that 1GB of RAM is wasteful,
189189+10GB is unacceptable. Bobbin also must serve users, whose each API
190190+request puts more pressure on the RAM beyond just storing the Tangled
191191+dataset.
192192+193193+There you have it! Now there's this fun AppView with no disk storage,
194194+sub-90-second backfill, that serves API requests as a little engine
195195+that could. With it, anyone can build programs using the Tangled dataset
196196+more easily.
197197+198198+There is an instance of Bobbin on
199199+[api.tangled.org](https://api.tangled.org), and hopefully you'd find it
200200+easy to host your own.