···11+# Logs
22+logs
33+*.log
44+npm-debug.log*
55+yarn-debug.log*
66+yarn-error.log*
77+lerna-debug.log*
88+.pnpm-debug.log*
99+1010+# Diagnostic reports (https://nodejs.org/api/report.html)
1111+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
1212+1313+# Runtime data
1414+pids
1515+*.pid
1616+*.seed
1717+*.pid.lock
1818+1919+# Directory for instrumented libs generated by jscoverage/JSCover
2020+lib-cov
2121+2222+# Coverage directory used by tools like istanbul
2323+coverage
2424+*.lcov
2525+2626+# nyc test coverage
2727+.nyc_output
2828+2929+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
3030+.grunt
3131+3232+# Bower dependency directory (https://bower.io/)
3333+bower_components
3434+3535+# node-waf configuration
3636+.lock-wscript
3737+3838+# Compiled binary addons (https://nodejs.org/api/addons.html)
3939+build/Release
4040+4141+# Dependency directories
4242+node_modules/
4343+jspm_packages/
4444+4545+# Snowpack dependency directory (https://snowpack.dev/)
4646+web_modules/
4747+4848+# TypeScript cache
4949+*.tsbuildinfo
5050+5151+# Optional npm cache directory
5252+.npm
5353+5454+# Optional eslint cache
5555+.eslintcache
5656+5757+# Optional stylelint cache
5858+.stylelintcache
5959+6060+# Microbundle cache
6161+.rpt2_cache/
6262+.rts2_cache_cjs/
6363+.rts2_cache_es/
6464+.rts2_cache_umd/
6565+6666+# Optional REPL history
6767+.node_repl_history
6868+6969+# Output of 'npm pack'
7070+*.tgz
7171+7272+# Yarn Integrity file
7373+.yarn-integrity
7474+7575+# dotenv environment variable files
7676+.env
7777+.env.development.local
7878+.env.test.local
7979+.env.production.local
8080+.env.local
8181+8282+# parcel-bundler cache (https://parceljs.org/)
8383+.cache
8484+.parcel-cache
8585+8686+# Next.js build output
8787+.next
8888+out
8989+9090+# Nuxt.js build / generate output
9191+.nuxt
9292+dist
9393+9494+# Gatsby files
9595+.cache/
9696+# Comment in the public line in if your project uses Gatsby and not Next.js
9797+# https://nextjs.org/blog/next-9-1#public-directory-support
9898+# public
9999+100100+# vuepress build output
101101+.vuepress/dist
102102+103103+# vuepress v2.x temp and cache directory
104104+.temp
105105+.cache
106106+107107+# Docusaurus cache and generated files
108108+.docusaurus
109109+110110+# Serverless directories
111111+.serverless/
112112+113113+# FuseBox cache
114114+.fusebox/
115115+116116+# DynamoDB Local files
117117+.dynamodb/
118118+119119+# TernJS port file
120120+.tern-port
121121+122122+# Stores VSCode versions used for testing VSCode extensions
123123+.vscode-test
124124+125125+# yarn v2
126126+.yarn/cache
127127+.yarn/unplugged
128128+.yarn/build-state.yml
129129+.yarn/install-state.gz
130130+.pnp.*
+4-12
.env.example
···11-# Whichever port you want to run this on
21FEEDGEN_PORT=3000
33-44-# Set to something like db.sqlite to store persistently
55-FEEDGEN_SQLITE_LOCATION=":memory:"
66-77-# Don't change unless you're working in a different environment than the primary Bluesky network
22+DATABASE_URL="postgresql://foo:bar@localhost:5432/baz"
83FEEDGEN_SUBSCRIPTION_ENDPOINT="wss://bsky.social"
99-1010-# Set this to the hostname that you intend to run the service at
1111-FEEDGEN_HOSTNAME="example.com"
1212-1313-# Only use this if you want a service did different from did:web
1414-# FEEDGEN_SERVICE_DID="did:plc:abcde..."44+FEEDGEN_HOSTNAME=
55+HANDLE=
66+PASSWORD=
···11+# # syntax = docker/dockerfile:1
22+33+# # Adjust NODE_VERSION as desired
44+# ARG NODE_VERSION=18.16.0
55+# FROM node:${NODE_VERSION}-slim as base
66+77+88+99+# # Node.js app lives here
1010+# WORKDIR /app
1111+1212+# # Set production environment
1313+1414+1515+# ARG PNPM_VERSION=8.5.1
1616+# RUN npm install -g pnpm@$PNPM_VERSION
1717+1818+1919+# # Throw-away build stage to reduce size of final image
2020+# FROM base as build
2121+2222+# # Install packages needed to build node modules
2323+# RUN apt-get update -qq && \
2424+# apt-get install -y python-is-python3 pkg-config build-essential
2525+2626+# # Install node modules
2727+# COPY --link package.json pnpm-lock.yaml ./
2828+# RUN pnpm install --frozen-lockfile
2929+3030+# # Copy application code
3131+# COPY --link . .
3232+3333+# # Build application
3434+# RUN pnpm run build
3535+3636+# # Remove development dependencies
3737+# RUN pnpm prune --prod
3838+3939+4040+# # Final stage for app image
4141+# FROM base
4242+4343+# # Copy built application
4444+# COPY --from=build /app /app
4545+4646+# # Start the server by default, this can be overwritten at runtime
4747+# EXPOSE 3000
4848+# CMD [ "pnpm", "run", "start" ]
4949+5050+FROM node:18-alpine as builder
5151+RUN apk add --no-cache libc6-compat
5252+RUN corepack enable && corepack prepare pnpm@latest --activate
5353+COPY . /app
5454+WORKDIR /app
5555+RUN pnpm i && pnpm run build && pnpm prune --prod
5656+FROM node:18-alpine as base
5757+LABEL fly_launch_runtime="Node.js"
5858+ENV NODE_ENV=production
5959+RUN corepack enable && corepack prepare pnpm@latest --activate
6060+COPY --from=builder /app /app
6161+WORKDIR /app
6262+EXPOSE 3000
6363+CMD [ "pnpm", "run", "start-prod" ]
+20-14
README.md
···11# ATProto Feed Generator
2233-🚧 Work in Progress 🚧
33+🚧 Work in Progress 🚧
4455-We are actively developing Feed Generator integration into the Bluesky PDS. Though we are reasonably confident about the general shape and interfaces laid out here, these interfaces and implementation details _are_ subject to change.
55+We are actively developing Feed Generator integration into the Bluesky PDS. Though we are reasonably confident about the general shape and interfaces laid out here, these interfaces and implementation details _are_ subject to change.
6677In the meantime, we've put together this starter kit for devs. It doesn't do everything, but it should be enough to get you familiar with the system & started building!
88···1515A Feed Generator service can host one or more algorithms. The service itself is identified by DID, while each algorithm that it hosts is declared by a record in the repo of the account that created it. For instance, feeds offered by Bluesky will likely be declared in `@bsky.app`'s repo. Therefore, a given algorithm is identified by the at-uri of the declaration record. This declaration record includes a pointer to the service's DID along with some profile information for the feed.
16161717The general flow of providing a custom algorithm to a user is as follows:
1818+1819- A user requests a feed from their server (PDS) using the at-uri of the declared feed
1920- The PDS resolves the at-uri and finds the DID doc of the Feed Generator
2021- The PDS sends a `getFeedSkeleton` request to the service endpoint declared in the Feed Generator's DID doc
···32333334Next you will need to do two things:
34353535-1. Implement indexing logic in `src/subscription.ts`.
3636-3636+1. Implement indexing logic in `src/subscription.ts`.
3737+3738 This will subscribe to the repo subscription stream on startup, parse events & index them according to your provided logic.
383939402. Implement feed generation logic in `src/algos`
40414141- For inspiration, we've provided a very simple feed algorithm (`whats-alf`) that returns all posts related to the titular character of the TV show ALF.
4242+ For inspiration, we've provided a very simple feed algorithm (`whats-alf`) that returns all posts related to the titular character of the TV show ALF.
42434344 You can either edit it or add another algorithm alongside it. The types are in place an dyou will just need to return something that satisfies the `SkeletonFeedPost[]` type.
4445···6364The skeleton that a Feed Generator puts together is, in its simplest form, a list of post URIs.
64656566```ts
6666-[
6767- {post: 'at://did:example:1234/app.bsky.feed.post/1'},
6868- {post: 'at://did:example:1234/app.bsky.feed.post/2'},
6969- {post: 'at://did:example:1234/app.bsky.feed.post/3'}
6767+;[
6868+ { post: 'at://did:example:1234/app.bsky.feed.post/1' },
6969+ { post: 'at://did:example:1234/app.bsky.feed.post/2' },
7070+ { post: 'at://did:example:1234/app.bsky.feed.post/3' },
7071]
7172```
7273···102103Users are authenticated with a simple JWT signed by the user's repo signing key.
103104104105This JWT header/payload takes the format:
106106+105107```ts
106108const header = {
107107- type: "JWT",
108108- alg: "ES256K" // (key algorithm) - in this case secp256k1
109109+ type: 'JWT',
110110+ alg: 'ES256K', // (key algorithm) - in this case secp256k1
109111}
110112const payload = {
111111- iss: "did:example:alice", // (issuer) the requesting user's DID
112112- aud: "did:example:feedGenerator", // (audience) the DID of the Feed Generator
113113- exp: 1683643619 // (expiration) unix timestamp in seconds
113113+ iss: 'did:example:alice', // (issuer) the requesting user's DID
114114+ aud: 'did:example:feedGenerator', // (audience) the DID of the Feed Generator
115115+ exp: 1683643619, // (expiration) unix timestamp in seconds
114116}
115117```
116118117119We provide utilities for verifying user JWTs in the `@atproto/xrpc-server` package, and you can see them in action in `src/auth.ts`.
118120119121### Pagination
122122+120123You'll notice that the `getFeedSkeleton` method returns a `cursor` in its response & takes a `cursor` param as input.
121124122125This cursor is treated as an opaque value & fully at the Feed Generator's discretion. It is simply pased through the PDS directly to & from the client.
···137140Some examples:
138141139142### Reimplementing What's Hot
143143+140144To reimplement "What's Hot", you may subscribe to the firehose & filter for all posts & likes (ignoring profiles/reposts/follows/etc). You would keep a running tally of likes per post & when a PDS requests a feed, you would send the most recent posts that pass some threshold of likes.
141145142146### A Community Feed
147147+143148You might create a feed for a given community by compiling a list of DIDs within that community & filtering the firehose for all posts from users within that list.
144149145150### A Topical Feed
151151+146152To implement a topical feed, you might filter the algorithm for posts and pass the post text through some filtering mechanism (an LLM, a keyword matcher, etc.) that filters for the topic of your choice.
+14
fly.toml
···11+# fly.toml app configuration file generated for feeds on 2023-05-23T19:11:41+01:00
22+#
33+# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
44+#
55+66+app = "feeds"
77+primary_region = "lhr"
88+99+[http_service]
1010+ internal_port = 3000
1111+ force_https = true
1212+ auto_stop_machines = true
1313+ auto_start_machines = true
1414+ min_machines_running = 0
···8899 // YOUR bluesky handle
1010 // Ex: user.bsky.social
1111- const handle = ''
1111+ const handle = process.env.HANDLE!
12121313 // YOUR bluesky password, or preferably an App Password (found in your client settings)
1414 // Ex: abcd-1234-efgh-5678
1515- const password = ''
1515+ const password = process.env.PASSWORD!
16161717 // A short name for the record that will show in urls
1818 // Lowercase with no spaces.
1919 // Ex: whats-hot
2020- const recordName = ''
2020+ const recordName = 'whos-alice'
21212222 // A display name for your feed
2323 // Ex: What's Hot
2424- const displayName = ''
2424+ const displayName = "Who's Alice"
25252626 // (Optional) A description of your feed
2727 // Ex: Top trending content from the whole network
2828- const description = ''
2828+ const description = 'Tweets from all the Alices.'
29293030 // (Optional) The path to an image to be used as your feed's avatar
3131 // Ex: ~/path/to/avatar.jpeg
3232- const avatar: string = ''
3232+ const avatar = 'scripts/sun.png'
33333434 // -------------------------------------
3535 // NO NEED TO TOUCH ANYTHING BELOW HERE
scripts/sun.png
This is a binary file and will not be displayed.
+2-2
src/algos/index.ts
···33 QueryParams,
44 OutputSchema as AlgoOutput,
55} from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
66-import * as whatsAlf from './whats-alf'
66+import * as whosAlice from './whos-alice'
7788type AlgoHandler = (ctx: AppContext, params: QueryParams) => Promise<AlgoOutput>
991010const algos: Record<string, AlgoHandler> = {
1111- [whatsAlf.uri]: whatsAlf.handler,
1111+ [whosAlice.uri]: whosAlice.handler,
1212}
13131414export default algos
+2-1
src/algos/whats-alf.ts
src/algos/whos-alice.ts
···22import { QueryParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
33import { AppContext } from '../config'
4455-export const uri = 'at://did:example:alice/app.bsky.feed.generator/whats-alf'
55+export const uri =
66+ 'at://did:web:feeds.bsky.sh/app.bsky.feed.generator/whos-alice'
6778export const handler = async (ctx: AppContext, params: QueryParams) => {
89 let builder = ctx.db