···4455- Self hostable: handles the full write throughput of the global atproto firehose on a raspberry pi 4b + single SSD
66- Storage efficient: less than 2GB/day disk consumption indexing all references in all lexicons and all non-atproto URLs
77+- Handles record deletion, account de/re-activation, and account deletion, ensuring accurate link counts and respecting users data choices
78- Simple JSON API
89910All social interactions in atproto tend to be represented by links (or references) between PDS records. This index can answer questions like "how many likes does a bsky post have", "who follows an account", "what are all the comments on a [frontpage](https://frontpage.fyi/) post", and more.
+7-4
constellation/src/server/mod.rs
···44use serde::{Deserialize, Serialize};
55use serde_with::serde_as;
66use std::collections::HashMap;
77-use std::time::{UNIX_EPOCH, Duration};
77+use std::time::{Duration, UNIX_EPOCH};
88use tokio::net::{TcpListener, ToSocketAddrs};
99use tokio::task::block_in_place;
1010use tokio_util::sync::CancellationToken;
···2121const DEFAULT_CURSOR_LIMIT_MAX: u64 = 100;
22222323const INDEX_BEGAN_AT_TS: u64 = 1738083600; // TODO: not this
2424-25242625pub async fn serve<S, A>(store: S, addr: A, stay_alive: CancellationToken) -> anyhow::Result<()>
2726where
···106105 days_indexed: u64,
107106 stats: StorageStats,
108107}
109109-fn hello(accept: ExtractAccept, store: impl LinkReader) -> Result<impl IntoResponse, http::StatusCode> {
108108+fn hello(
109109+ accept: ExtractAccept,
110110+ store: impl LinkReader,
111111+) -> Result<impl IntoResponse, http::StatusCode> {
110112 let stats = store
111113 .get_stats()
112114 .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?;
113115 let days_indexed = (UNIX_EPOCH + Duration::from_secs(INDEX_BEGAN_AT_TS))
114116 .elapsed()
115117 .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?
116116- .as_secs() / 86400;
118118+ .as_secs()
119119+ / 86400;
117120 Ok(acceptable(accept, HelloReponse {
118121 help: "open this URL in a web browser (or request with Accept: text/html) for information about this API.",
119122 days_indexed,
+1-1
constellation/src/storage/mod.rs
···11use anyhow::Result;
22use constellation::{ActionableEvent, CountsByCount, Did, RecordId};
33-use std::collections::HashMap;
43use serde::{Deserialize, Serialize};
44+use std::collections::HashMap;
5566pub mod mem_store;
77pub use mem_store::MemStorage;
+2-1
constellation/templates/base.html.j2
···22<html lang="en">
33 <head>
44 <meta charset="utf8">
55- <title>{% block title %}{% endblock %} — Link Aggregator</title>
55+ <title>{% block title %}{% endblock %} — Constellation</title>
66 <meta name="viewport" content="width=device-width, initial-scale=1" />
77+ <meta name="description" content="{% block description %}Constellation is a self-hosted JSON API to an atproto-wide index of PDS record back-links. You can use it to query social interactions in real time.{% endblock %}" />
78 <style>
89 body {
910 font-family: sans-serif;
+2-1
constellation/templates/dids-count.html.j2
···11{% extends "base.html.j2" %}
22{% import "try-it-macros.html.j2" as try_it %}
3344-{% block title %}Link count{% endblock %}
44+{% block title %}Distinct DIDs{% endblock %}
55+{% block description %}Count of distinct DIDs with {{ query.collection }} records linking to {{ query.target }} at JSON path {{ query.path }}{% endblock %}
5667{% block content %}
78
+1
constellation/templates/dids.html.j2
···22{% import "try-it-macros.html.j2" as try_it %}
3344{% block title %}DIDs{% endblock %}
55+{% block description %}All distinct DIDs with {{ query.collection }} records linking to {{ query.target }} at JSON path {{ query.path }}{% endblock %}
5667{% block content %}
78
+1
constellation/templates/explore-links.html.j2
···22{% import "try-it-macros.html.j2" as try_it %}
3344{% block title %}Explore links{% endblock %}
55+{% block description %}All link sources of atproto records linking to {{ query.target }}{% endblock %}
5667{% block content %}
78
+2
constellation/templates/hello.html.j2
···1616 <li>and more</li>
1717 </ul>
18181919+ <p>It works by recursively walking <em>all</em> records coming through the firehose, searching for anything that looks like a link. Links are indexed by the target they point at, the collection the record came from, and the JSON path to the link in that record.</p>
2020+1921 <p>
2022 This server has indexed <span class="stat">{{ stats.linking_records|human_number }}</span> links between <span class="stat">{{ stats.targetables|human_number }}</span> targets and sources from <span class="stat">{{ stats.dids|human_number }}</span> identities over <span class="stat">{{ days_indexed|human_number }}</span> days.<br/>
2123 <small>(indexing new records in real time, backfill still TODO)</small>
+1
constellation/templates/links-count.html.j2
···22{% import "try-it-macros.html.j2" as try_it %}
3344{% block title %}Link count{% endblock %}
55+{% block description %}Count of {{ query.collection }} records linking to {{ query.target }} at JSON path {{ query.path }}{% endblock %}
5667{% block content %}
78
+1
constellation/templates/links.html.j2
···22{% import "try-it-macros.html.j2" as try_it %}
3344{% block title %}Links{% endblock %}
55+{% block description %}All {{ query.collection }} records with links to {{ query.target }} at JSON path {{ query.path }}{% endblock %}
5667{% block content %}
78
+1
readme.md
···11111212- Self hostable: handles the full write throughput of the global atproto firehose on a raspberry pi 4b + single SSD
1313- Storage efficient: less than 2GB/day disk consumption indexing all references in all lexicons and all non-atproto URLs
1414+- Handles record deletion, account de/re-activation, and account deletion, ensuring accurate link counts and respecting users data choices
1415- Simple JSON API
15161617All social interactions in atproto tend to be represented by links (or references) between PDS records. This index can answer questions like "how many likes does a bsky post have", "who follows an account", "what are all the comments on a [frontpage](https://frontpage.fyi/) post", and more.