Glean
A social RSS reader built on the AT Protocol. Sign in with your Bluesky/Atmosphere account, subscribe to feeds, and discover what like-minded readers are into.
Try it at glean.at.
Your subscriptions live as records on your PDS. You own them. If Glean goes away, your data doesn't.
What you get
- RSS, Atom, and JSON Feed support with a keyboard-driven reading interface
- Highlights, notes, tags, and ratings on articles
- margin.at annotations displayed alongside glean annotations
- A trending page showing what's popular across all users
- Feed and people recommendations based on reading overlap
- Daily digest with an AI-generated summary of your unread articles
- OPML import and export
- Sign in with Bluesky / Atmosphere account — no new account needed
How recommendations work
Glean looks at what you and other users subscribe to, read, and like to suggest feeds, articles, and people you might enjoy.
Feed suggestions come from readers who share your subscriptions. If a lot of people who follow the same blogs as you also follow a blog you haven't seen, that blog shows up as a recommendation. The system also considers which articles you've liked, whether you follow the person on Bluesky, and how popular the feed is overall.
People suggestions are split into two groups: "Your network" shows people you already follow on Bluesky who share your reading habits, and "Discover new readers" surfaces readers you don't follow but who have overlapping subscriptions and likes.
Dismissals keep things tidy. If you dismiss a recommendation, it won't come back. If a suggestion sits ignored for more than 5 days, it's automatically removed so newer recommendations can take its place.
Cold start. If you're new and have fewer than five subscriptions, Glean shows feeds from people you follow on Bluesky alongside popular feeds from the community, so there's something to explore right away.
The system improves over time: as you subscribe to feeds and like articles, Glean learns which signals matter most to you and adjusts accordingly.
Self-hosting
Docker
docker run -p 3000:3000 -e GLEAN_SESSION_KEY=changeme -v glean-data:/data atcr.io/julien.rbrt.fr/glean:latest
The published image is built from this repo's flake — nix build .#image
produces a script that streams a docker-archive tarball:
nix run .#image > glean-image.tar && docker load < glean-image.tar
make image wraps that, and falls back to building on a remote nix builder
($GLEAN_NIX_VM, default nix-vm) when the local machine has no nix.
From source
The frontend is a SvelteKit app in web/; the Go binary serves a JSON API.
SvelteKit runs the SSR server (port 3000) and proxies /api to the Go API
(port 8080).
Everything is packaged as a nix flake, so with nix installed:
nix build .#glean # the Go binary
nix build .#frontend # the built SvelteKit app
nix develop # a shell with go, gcc, bun, node and the CGO flags set
Without nix, the Makefile does the same thing against your own toolchain:
git clone https://github.com/anomalyco/glean.git
cd glean
make web-install # install frontend deps (bun)
make build # builds the frontend and the Go binary
Run both in dev:
make dev-api # Go API on :8080
make dev-web # SvelteKit dev server on :3000 (proxies /api to :8080)
Then open http://localhost:3000.
Configuration
| Variable | Default | What it does |
|---|---|---|
GLEAN_SESSION_KEY |
(required) | Secret key for signing session cookies (any random string) |
GLEAN_ADDR |
:8080 |
Listen address |
GLEAN_DB |
glean.db |
SQLite base path (_users, _articles, _recs suffixes) |
GLEAN_JETSTREAM |
wss://jetstream1.eurosky.network |
Jetstream WebSocket URL |
GLEAN_SYNC_INTERVAL |
8h |
PDS sync interval (Go duration: 24h, 12h, etc.) |
GLEAN_CLUSTER_INTERVAL |
1h |
Cluster recomputation interval (Go duration) |
GLEAN_FETCH_INTERVAL |
15m |
Feed fetch scheduler tick interval (Go duration) |
GLEAN_COLLECTION_DIR_URL |
(empty) | Collection directory URL for startup backfill |
GLEAN_BACKFILL_CONCURRENCY |
5 |
Max concurrent backfill workers |
GLEAN_ARTICLE_RETENTION_DAYS |
30 |
Delete articles older than this many days; jetstream only streams events of known users |
GLEAN_PLC_URL |
https://plc.eurosky.network |
PLC directory URL for DID resolution |
GLEAN_OAUTH_CLIENT_ID |
(empty) | OAuth client-metadata URL; enables production OAuth (leave empty for localhost dev). Must resolve to this server's /api/oauth/client-metadata |
GLEAN_FRONTEND_URL |
(required) | Public origin of the SvelteKit frontend (e.g. https://glean.at); make dev defaults this to http://localhost:3000 |
GLEAN_EMBED_BASE_URL |
(empty) | Embeddings API base URL (recommended, see below) |
GLEAN_EMBED_API_KEY |
(empty) | API key for the embeddings endpoint |
GLEAN_EMBED_MODEL |
text-embedding-3-small |
Embedding model name |
GLEAN_EMBED_DIMENSION |
1536 |
Embedding vector dimension |
GLEAN_LLM_BASE_URL |
(empty) | LLM API base URL for language detection and digest summaries (see below) |
GLEAN_LLM_API_KEY |
(empty) | API key for the LLM endpoint |
GLEAN_LLM_MODEL |
gpt-4o-mini |
LLM model name |
GLEAN_PPROF_ADDR |
(empty) | Enable pprof profiling server (e.g. :6060, off by default) |
For production:
export GLEAN_OAUTH_CLIENT_ID=https://yourdomain.com/api/oauth/client-metadata
The OAuth callback is always served at $GLEAN_FRONTEND_URL/api/auth/callback (the frontend proxies it to the backend), so there is no separate redirect-URL setting.
The SvelteKit server reads GLEAN_API_URL (default http://localhost:8080) to
find the Go API and PORT for its listen address. The public origin lives in
GLEAN_FRONTEND_URL; SvelteKit's adapter-node ORIGIN is not needed here.
Documentation
- Technical specification — architecture, database schema, AT Protocol lexicons, API endpoints, recommendations
- Design system
Stack
Go, SQLite, SvelteKit (SSR), TailwindCSS, AT Protocol OAuth.
License
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
|