···11+## My Appview
22+33+This is a small(ish) appview meant for a single user (me or you) that can be spun up at any time and used as a basic appview to see Bluesky posts for users that single user follows.
44+55+In April 2026 Bluesky had an outage which caused the appview to be unavailable, which those that used the appview could not use Bluesky the app. Workarounds included switching to a different appview (such as Blacksky) or using a different client that used a different appview (such as Blacksky or Red Dwarf).
66+77+This appview intends to be one of those different appviews that can be used in case of an outage.
88+99+When the appview first starts it will look for the user that the appview is intended for and get a list of that users follows. It will then attempt to backfill and index all of those follow users posts and then keep track and index live posts as they happen.
1010+1111+If the user follows or unfollows a user, that will be reflected in the appviews index.
1212+1313+It is not (at the moment) supposed to be a complex appview, just a way for a user to still view the Bluesky network for the users they follow.
1414+1515+### Todo
1616+1717+- [ ] - Configure Tap
1818+ - [ ] Run as part of a docker-compose file
1919+ - [ ] Configure to be in `Dynamically Configured` mode
2020+ - [ ] Configure the filters to be for `app.bsky.*` (maybe limit this just to be the ones needed)
2121+- [ ] - App start up configuration for user
2222+ - [ ] Get users did from config
2323+ - [ ] Fetch and store that users follows
2424+ - [ ] For each user call the `/repos/add` endpoint on tap to add the follow to be tracked
2525+ - [ ] Call `/repos/add` for the user of the appview
2626+- [ ] - Handle the events for tracked users
2727+ - [ ] Ignore anything other than the post lexicon types for the follows (work out what lexicon types need to be used for the user using the appview)
2828+ - [ ] Ignore if older than x amount of days (configurable number of days) - appview doesn't really need full history, only fairly recent and live
2929+ - [ ] Store and index the data in sqlite (schema TBD)
3030+ - [ ] MAYBE..... If the post is part of a reply, quote etc, maybe all relevant posts should be fetched too? Not sure how that will look yet, but that will be needed to render the posts in the app correctly I think.
3131+- [ ] - Start to implement the endpoints required to make this an appview (looking at a [similar project konbini](https://tangled.org/why.bsky.team/konbini/) by [@why.bsky.team](https://bsky.app/profile/why.bsky.team))
3232+ - [x] `GET /.well-known/did.json`
3333+ - [ ] `GET /xrpc/com.atproto.identity.resolveHandle`
3434+ - [ ] `GET /xrpc/com.atproto.repo.getRecord`
3535+ - [ ] `GET /xrpc/app.bsky.actor.getProfile`
3636+ - [ ] `GET /xrpc/app.bsky.actor.getProfiles`
3737+ - [ ] `GET /xrpc/app.bsky.actor.getPreferences`
3838+ - [ ] `POST /xrpc/app.bsky.actor.putPreferences`
3939+ - [ ] `GET /xrpcapp.bsky.actor.searchActors`
4040+ - [ ] `GET /xrpc/app.bsky.actor.searchActorsTypeahead`
4141+ - [ ] `GET /xrpc/app.bsky.feed.getTimeline`
4242+ - [ ] `GET /xrpc/app.bsky.feed.getAuthorFeed`
4343+ - [ ] `GET /xrpc/app.bsky.feed.getPostThread`
4444+ - [ ] `GET /xrpc/app.bsky.feed.getPosts`
4545+ - [ ] `GET /xrpc/app.bsky.feed.getLikes`
4646+ - [ ] `GET /xrpc/app.bsky.feed.getRepostedBy`
4747+ - [ ] `GET /xrpc/app.bsky.feed.getActorLikes`
4848+ - [ ] `GET /xrpc/app.bsky.feed.getFeed`
4949+ - [ ] `GET /xrpc/app.bsky.feed.getFeedGenerator`
5050+ - [ ] `GET /xrpc/app.bsky.graph.getFollows`
5151+ - [ ] `GET /xrpc/app.bsky.graph.getFollowers`
5252+ - [ ] `GET /xrpc/app.bsky.graph.getBlocks`
5353+ - [ ] `GET /xrpc/app.bsky.graph.getMutes`
5454+ - [ ] `GET /xrpc/app.bsky.graph.getRelationships`
5555+ - [ ] `GET /xrpc/app.bsky.graph.getLists`
5656+ - [ ] `GET /xrpc/app.bsky.graph.getList`
5757+ - [ ] `GET /xrpc/app.bsky.notification.listNotifications`
5858+ - [ ] `GET /xrpc/app.bsky.notification.getUnreadCount`
5959+ - [ ] `POST /xrpc/app.bsky.notification.updateSeen`
6060+ - [ ] `GET /xrpc/app.bsky.labeler.getServices`
6161+ - [ ] `GET /xrpc/app.bsky.unspecced.getConfig`
6262+ - [ ] `GET /xrpc/app.bsky.unspecced.getTrendingTopics`
6363+ - [ ] `GET /xrpcapp.bsky.unspecced.getPostThreadV2`
+73
src/main.rs
···11+use axum::{
22+ Json, Router,
33+ response::{IntoResponse, Response},
44+ routing::get,
55+};
66+use serde_json::json;
77+use std::net::SocketAddr;
88+99+#[tokio::main]
1010+async fn main() -> anyhow::Result<()> {
1111+ dotenv::dotenv().ok();
1212+1313+ let host = std::env::var("APPVIEW_HOST").unwrap_or("0.0.0.0".to_string());
1414+ let port: u16 = std::env::var("APPVIEW_PORT")
1515+ .ok()
1616+ .and_then(|s| s.parse().ok())
1717+ .unwrap_or(3000);
1818+1919+ let app = Router::new()
2020+ .route("/", get(say_hello_text))
2121+ .route("/.well-known/did.json", get(well_known_did_json));
2222+2323+ let addr: SocketAddr = format!("{host}:{port}")
2424+ .parse()
2525+ .expect("valid socket address");
2626+2727+ println!("listening on {addr}");
2828+2929+ let listener = tokio::net::TcpListener::bind(addr.to_string())
3030+ .await
3131+ .unwrap();
3232+ axum::serve(listener, app).await.unwrap();
3333+3434+ Ok(())
3535+}
3636+3737+async fn say_hello_text() -> &'static str {
3838+ return "This is an appview. Work in progress. This is my appview. There are many like it, but this one is mine";
3939+}
4040+4141+async fn well_known_did_json() -> Response {
4242+ // TODO: work out how to pass these env from the main function
4343+ let appview_did = std::env::var("APPVIEW_DID").unwrap_or("did:web:localhost".to_string());
4444+ let appview_endpoint = std::env::var("APPVIEW_HOSTNAME").unwrap_or("localhost".to_string());
4545+4646+ Json(json!({
4747+ "@context": [
4848+ "https://www.w3.org/ns/did/v1",
4949+ "https://w3id.org/security/multikey/v1"],
5050+ "id": appview_did,
5151+ "verificationMethod": [
5252+ {
5353+ "id": "did:web:api.bsky.app#atproto",
5454+ "type": "Multikey",
5555+ "controller": "did:web:api.bsky.app",
5656+ "publicKeyMultibase": "zQ3shpRzb2NDriwCSSsce6EqGxG23kVktHZc57C3NEcuNy1jg"
5757+ }
5858+ ],
5959+ "service": [
6060+ {
6161+ "id": "#bsky_notif",
6262+ "type": "BskyNotificationService",
6363+ "serviceEndpoint": appview_endpoint
6464+ },
6565+ {
6666+ "id": "#bsky_appview",
6767+ "type": "BskyAppView",
6868+ "serviceEndpoint": appview_endpoint
6969+ }
7070+ ]
7171+ }))
7272+ .into_response()
7373+}