nandi/gleanpublic⑂ Fork 0
⑂ 016bf11
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Switch to own Jetstream and PLC endpointsUnverified

Julien Robert committed 2026-04-21T11:24:52+02:00 Browse files
016bf11 parent: e230487
modified .env.example +2 -1
@@ -1,6 +1,7 @@
11 GLEAN_ADDR=:8080
22 GLEAN_DB=glean.db
3-GLEAN_JETSTREAM=wss://jetstream2.fr.hose.cam
3+GLEAN_JETSTREAM=wss://jetstream.glean.at
4+GLEAN_PLC_URL=https://didplc.glean.at
45 GLEAN_SYNC_INTERVAL=1h
56 GLEAN_CLUSTER_INTERVAL=6h
67 # Leave empty for localhost OAuth (development)
@@ -1,6 +1,7 @@
1 GLEAN_ADDR=:80801 GLEAN_ADDR=:8080
2 GLEAN_DB=glean.db2 GLEAN_DB=glean.db
3-GLEAN_JETSTREAM=wss://jetstream2.fr.hose.cam3+GLEAN_JETSTREAM=wss://jetstream.glean.at
4+GLEAN_PLC_URL=https://didplc.glean.at
4 GLEAN_SYNC_INTERVAL=1h5 GLEAN_SYNC_INTERVAL=1h
5 GLEAN_CLUSTER_INTERVAL=6h6 GLEAN_CLUSTER_INTERVAL=6h
6 # Leave empty for localhost OAuth (development)7 # Leave empty for localhost OAuth (development)
modified docs/specs.md +34 -12
@@ -10,14 +10,14 @@ The core idea: your RSS subscriptions are a strong signal about your interests.
1010
1111 ## 2. Stack
1212
13-| Layer | Technology |
14-| ---------------- | ------------------------------------ |
15-| Backend | Go |
16-| Database | SQLite (via `mattn/go-sqlite3`) |
17-| Frontend | htmx + TailwindCSS |
18-| Auth | AT Protocol OAuth / DID resolution |
19-| AT Protocol role | AppView for `at.glean.*` lexicons |
20-| Data source | AT Protocol Jetstream → SQLite index |
13+| Layer | Technology |
14+| ---------------- | --------------------------------------------------------------- |
15+| Backend | Go |
16+| Database | SQLite (via `mattn/go-sqlite3`) |
17+| Frontend | htmx + TailwindCSS |
18+| Auth | AT Protocol OAuth / DID resolution (configurable PLC directory) |
19+| AT Protocol role | AppView for `at.glean.*` lexicons |
20+| Data source | AT Protocol Jetstream → SQLite index |
2121
2222 ## 3. AT Protocol Lexicons
2323
@@ -137,7 +137,26 @@ The mapping from margin note to glean annotation:
137137
138138 When no matching article exists in the local DB, the annotation is stored with an empty `feed_url`. Margin notes are indexed from both Jetstream and PDS sync, same as glean records.
139139
140-### 3.5 AppView Query Lexicons
140+### 3.5 `app.bsky.graph.follow` (External)
141+
142+Follow relationships are tracked from Bluesky and Tangled follow records. The `FollowRecord` struct is validated against the lexicon at `lexicons/app/bsky/graph/follow.json`. The optional `via` field (a strong ref) is preserved as raw JSON but not used by Glean.
143+
144+### 3.6 Lexicon Constants
145+
146+All collection NSIDs are defined as constants in `lexicon.go` and used throughout the codebase:
147+
148+```go
149+const (
150+ CollectionSubscription = "at.glean.subscription"
151+ CollectionAnnotation = "at.glean.annotation"
152+ CollectionLike = "at.glean.like"
153+ CollectionMarginNote = "at.margin.note"
154+ CollectionBskyFollow = "app.bsky.graph.follow"
155+ CollectionTangledFollow = "sh.tangled.graph.follow"
156+)
157+```
158+
159+### 3.7 AppView Query Lexicons
141160
142161 As an AppView, Glean serves the following XRPC query endpoints. Other AT Protocol applications can call these to access indexed `at.glean.*` data without implementing their own indexer.
143162
@@ -234,9 +253,9 @@ Output:
234253 people: [{ did, handle, displayName, avatar, jaccard, commonFeeds }]
235254 ```
236255
237-### 3.6 AppView Jetstream Consumption
256+### 3.8 AppView Jetstream Consumption
238257
239-Glean subscribes to a Jetstream endpoint (`GLEAN_JETSTREAM`, default `wss://jetstream2.fr.hose.cam`) for all `at.glean.*` records:
258+Glean subscribes to a Jetstream endpoint (`GLEAN_JETSTREAM`, default `wss://jetstream.glean.at`) for all `at.glean.*` records:
240259
241260 ```
242261 SUBSCRIBE collections: ["at.glean.subscription", "at.glean.annotation", "at.glean.like", "app.bsky.graph.follow", "sh.tangled.graph.follow", "at.margin.note"]
@@ -774,7 +793,8 @@ glean/
774793 ├── lexicons/
775794 │ └── at/
776795 │ ├── glean/ # Glean lexicon JSON schemas (subscription, annotation, like)
777-│ └── margin/ # External lexicon schemas (note.json for at.margin.note)
796+│ └── margin/ # External: at.margin.note W3C Web Annotation schema
797+│ └── app/bsky/graph/ # External: app.bsky.graph.follow schema
778798 ├── internal/
779799 │ ├── atproto/
780800 │ │ ├── auth.go # DID resolution, OAuth flow
@@ -846,6 +866,8 @@ glean/
846866
847867 ## 10. Auth Flow
848868
869+DID resolution uses a configurable PLC directory (`GLEAN_PLC_URL`, defaults to `https://didplc.glean.at`). The identity directory is initialized once at startup via `InitIdentity()` with a caching layer (250k entries, 24h TTL).
870+
849871 1. User visits `/`, clicks "Sign in with Bluesky" (or any AT Proto PDS)
850872 2. Server redirects to AT Protocol OAuth authorization endpoint
851873 3. User authorizes on their PDS
@@ -10,14 +10,14 @@ The core idea: your RSS subscriptions are a strong signal about your interests.
10 10
11 ## 2. Stack11 ## 2. Stack
12 12
13-| Layer | Technology |13+| Layer | Technology |
14-| ---------------- | ------------------------------------ |14+| ---------------- | --------------------------------------------------------------- |
15-| Backend | Go |15+| Backend | Go |
16-| Database | SQLite (via `mattn/go-sqlite3`) |16+| Database | SQLite (via `mattn/go-sqlite3`) |
17-| Frontend | htmx + TailwindCSS |17+| Frontend | htmx + TailwindCSS |
18-| Auth | AT Protocol OAuth / DID resolution |18+| Auth | AT Protocol OAuth / DID resolution (configurable PLC directory) |
19-| AT Protocol role | AppView for `at.glean.*` lexicons |19+| AT Protocol role | AppView for `at.glean.*` lexicons |
20-| Data source | AT Protocol Jetstream → SQLite index |20+| Data source | AT Protocol Jetstream → SQLite index |
21 21
22 ## 3. AT Protocol Lexicons22 ## 3. AT Protocol Lexicons
23 23
@@ -137,7 +137,26 @@ The mapping from margin note to glean annotation:
137 137
138 When no matching article exists in the local DB, the annotation is stored with an empty `feed_url`. Margin notes are indexed from both Jetstream and PDS sync, same as glean records.138 When no matching article exists in the local DB, the annotation is stored with an empty `feed_url`. Margin notes are indexed from both Jetstream and PDS sync, same as glean records.
139 139
140-### 3.5 AppView Query Lexicons140+### 3.5 `app.bsky.graph.follow` (External)
141+
142+Follow relationships are tracked from Bluesky and Tangled follow records. The `FollowRecord` struct is validated against the lexicon at `lexicons/app/bsky/graph/follow.json`. The optional `via` field (a strong ref) is preserved as raw JSON but not used by Glean.
143+
144+### 3.6 Lexicon Constants
145+
146+All collection NSIDs are defined as constants in `lexicon.go` and used throughout the codebase:
147+
148+```go
149+const (
150+ CollectionSubscription = "at.glean.subscription"
151+ CollectionAnnotation = "at.glean.annotation"
152+ CollectionLike = "at.glean.like"
153+ CollectionMarginNote = "at.margin.note"
154+ CollectionBskyFollow = "app.bsky.graph.follow"
155+ CollectionTangledFollow = "sh.tangled.graph.follow"
156+)
157+```
158+
159+### 3.7 AppView Query Lexicons
141 160
142 As an AppView, Glean serves the following XRPC query endpoints. Other AT Protocol applications can call these to access indexed `at.glean.*` data without implementing their own indexer.161 As an AppView, Glean serves the following XRPC query endpoints. Other AT Protocol applications can call these to access indexed `at.glean.*` data without implementing their own indexer.
143 162
@@ -234,9 +253,9 @@ Output:
234 people: [{ did, handle, displayName, avatar, jaccard, commonFeeds }]253 people: [{ did, handle, displayName, avatar, jaccard, commonFeeds }]
235 ```254 ```
236 255
237-### 3.6 AppView Jetstream Consumption256+### 3.8 AppView Jetstream Consumption
238 257
239-Glean subscribes to a Jetstream endpoint (`GLEAN_JETSTREAM`, default `wss://jetstream2.fr.hose.cam`) for all `at.glean.*` records:258+Glean subscribes to a Jetstream endpoint (`GLEAN_JETSTREAM`, default `wss://jetstream.glean.at`) for all `at.glean.*` records:
240 259
241 ```260 ```
242 SUBSCRIBE collections: ["at.glean.subscription", "at.glean.annotation", "at.glean.like", "app.bsky.graph.follow", "sh.tangled.graph.follow", "at.margin.note"]261 SUBSCRIBE collections: ["at.glean.subscription", "at.glean.annotation", "at.glean.like", "app.bsky.graph.follow", "sh.tangled.graph.follow", "at.margin.note"]
@@ -774,7 +793,8 @@ glean/
774 ├── lexicons/793 ├── lexicons/
775 │ └── at/794 │ └── at/
776 │ ├── glean/ # Glean lexicon JSON schemas (subscription, annotation, like)795 │ ├── glean/ # Glean lexicon JSON schemas (subscription, annotation, like)
777-│ └── margin/ # External lexicon schemas (note.json for at.margin.note)796+│ └── margin/ # External: at.margin.note W3C Web Annotation schema
797+│ └── app/bsky/graph/ # External: app.bsky.graph.follow schema
778 ├── internal/798 ├── internal/
779 │ ├── atproto/799 │ ├── atproto/
780 │ │ ├── auth.go # DID resolution, OAuth flow800 │ │ ├── auth.go # DID resolution, OAuth flow
@@ -846,6 +866,8 @@ glean/
846 866
847 ## 10. Auth Flow867 ## 10. Auth Flow
848 868
869+DID resolution uses a configurable PLC directory (`GLEAN_PLC_URL`, defaults to `https://didplc.glean.at`). The identity directory is initialized once at startup via `InitIdentity()` with a caching layer (250k entries, 24h TTL).
870+
849 1. User visits `/`, clicks "Sign in with Bluesky" (or any AT Proto PDS)871 1. User visits `/`, clicks "Sign in with Bluesky" (or any AT Proto PDS)
850 2. Server redirects to AT Protocol OAuth authorization endpoint872 2. Server redirects to AT Protocol OAuth authorization endpoint
851 3. User authorizes on their PDS873 3. User authorizes on their PDS
modified internal/atproto/auth.go +47 -6
@@ -3,6 +3,10 @@ package atproto
33 import (
44 "context"
55 "fmt"
6+ "net"
7+ "net/http"
8+ "os"
9+ "time"
610
711 "github.com/bluesky-social/indigo/atproto/identity"
812 "github.com/bluesky-social/indigo/atproto/syntax"
@@ -13,14 +17,53 @@ type (
1317 Identity = identity.Identity
1418 )
1519
20+var directory identity.Directory
21+
22+func InitIdentity(plcURL string) {
23+ if plcURL == "" {
24+ plcURL = identity.DefaultPLCURL
25+ }
26+ base := identity.BaseDirectory{
27+ PLCURL: plcURL,
28+ HTTPClient: http.Client{
29+ Timeout: 10 * time.Second,
30+ Transport: &http.Transport{
31+ Proxy: http.ProxyFromEnvironment,
32+ IdleConnTimeout: 1000 * time.Millisecond,
33+ MaxIdleConns: 100,
34+ },
35+ },
36+ Resolver: net.Resolver{
37+ Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
38+ d := net.Dialer{Timeout: 3 * time.Second}
39+ return d.DialContext(ctx, network, address)
40+ },
41+ },
42+ TryAuthoritativeDNS: true,
43+ SkipDNSDomainSuffixes: []string{".bsky.social"},
44+ UserAgent: "glean/1.0",
45+ }
46+ directory = identity.NewCacheDirectory(&base, 250_000, 24*time.Hour, 2*time.Minute, 5*time.Minute)
47+}
48+
49+func dir() identity.Directory {
50+ if directory == nil {
51+ plcURL := os.Getenv("GLEAN_PLC_URL")
52+ if plcURL == "" {
53+ plcURL = identity.DefaultPLCURL
54+ }
55+ InitIdentity(plcURL)
56+ }
57+ return directory
58+}
59+
1660 func ResolveHandle(ctx context.Context, handle string) (string, error) {
1761 h, err := syntax.ParseHandle(handle)
1862 if err != nil {
1963 return "", fmt.Errorf("parsing handle: %w", err)
2064 }
2165
22- dir := identity.DefaultDirectory()
23- ident, err := dir.LookupHandle(ctx, h)
66+ ident, err := dir().LookupHandle(ctx, h)
2467 if err != nil {
2568 return "", fmt.Errorf("resolving handle: %w", err)
2669 }
@@ -34,8 +77,7 @@ func ResolveDID(ctx context.Context, did string) (*DIDDocument, error) {
3477 return nil, fmt.Errorf("parsing DID: %w", err)
3578 }
3679
37- dir := identity.DefaultDirectory()
38- ident, err := dir.LookupDID(ctx, d)
80+ ident, err := dir().LookupDID(ctx, d)
3981 if err != nil {
4082 return nil, fmt.Errorf("resolving DID: %w", err)
4183 }
@@ -50,8 +92,7 @@ func ResolveIdentity(ctx context.Context, identifier string) (*Identity, error)
5092 return nil, fmt.Errorf("parsing identifier: %w", err)
5193 }
5294
53- dir := identity.DefaultDirectory()
54- ident, err := dir.Lookup(ctx, atid)
95+ ident, err := dir().Lookup(ctx, atid)
5596 if err != nil {
5697 return nil, fmt.Errorf("resolving identity: %w", err)
5798 }
@@ -3,6 +3,10 @@ package atproto
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "net"
7+ "net/http"
8+ "os"
9+ "time"
6 10
7 "github.com/bluesky-social/indigo/atproto/identity"11 "github.com/bluesky-social/indigo/atproto/identity"
8 "github.com/bluesky-social/indigo/atproto/syntax"12 "github.com/bluesky-social/indigo/atproto/syntax"
@@ -13,14 +17,53 @@ type (
13 Identity = identity.Identity17 Identity = identity.Identity
14 )18 )
15 19
20+var directory identity.Directory
21+
22+func InitIdentity(plcURL string) {
23+ if plcURL == "" {
24+ plcURL = identity.DefaultPLCURL
25+ }
26+ base := identity.BaseDirectory{
27+ PLCURL: plcURL,
28+ HTTPClient: http.Client{
29+ Timeout: 10 * time.Second,
30+ Transport: &http.Transport{
31+ Proxy: http.ProxyFromEnvironment,
32+ IdleConnTimeout: 1000 * time.Millisecond,
33+ MaxIdleConns: 100,
34+ },
35+ },
36+ Resolver: net.Resolver{
37+ Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
38+ d := net.Dialer{Timeout: 3 * time.Second}
39+ return d.DialContext(ctx, network, address)
40+ },
41+ },
42+ TryAuthoritativeDNS: true,
43+ SkipDNSDomainSuffixes: []string{".bsky.social"},
44+ UserAgent: "glean/1.0",
45+ }
46+ directory = identity.NewCacheDirectory(&base, 250_000, 24*time.Hour, 2*time.Minute, 5*time.Minute)
47+}
48+
49+func dir() identity.Directory {
50+ if directory == nil {
51+ plcURL := os.Getenv("GLEAN_PLC_URL")
52+ if plcURL == "" {
53+ plcURL = identity.DefaultPLCURL
54+ }
55+ InitIdentity(plcURL)
56+ }
57+ return directory
58+}
59+
16 func ResolveHandle(ctx context.Context, handle string) (string, error) {60 func ResolveHandle(ctx context.Context, handle string) (string, error) {
17 h, err := syntax.ParseHandle(handle)61 h, err := syntax.ParseHandle(handle)
18 if err != nil {62 if err != nil {
19 return "", fmt.Errorf("parsing handle: %w", err)63 return "", fmt.Errorf("parsing handle: %w", err)
20 }64 }
21 65
22- dir := identity.DefaultDirectory()66+ ident, err := dir().LookupHandle(ctx, h)
23- ident, err := dir.LookupHandle(ctx, h)
24 if err != nil {67 if err != nil {
25 return "", fmt.Errorf("resolving handle: %w", err)68 return "", fmt.Errorf("resolving handle: %w", err)
26 }69 }
@@ -34,8 +77,7 @@ func ResolveDID(ctx context.Context, did string) (*DIDDocument, error) {
34 return nil, fmt.Errorf("parsing DID: %w", err)77 return nil, fmt.Errorf("parsing DID: %w", err)
35 }78 }
36 79
37- dir := identity.DefaultDirectory()80+ ident, err := dir().LookupDID(ctx, d)
38- ident, err := dir.LookupDID(ctx, d)
39 if err != nil {81 if err != nil {
40 return nil, fmt.Errorf("resolving DID: %w", err)82 return nil, fmt.Errorf("resolving DID: %w", err)
41 }83 }
@@ -50,8 +92,7 @@ func ResolveIdentity(ctx context.Context, identifier string) (*Identity, error)
50 return nil, fmt.Errorf("parsing identifier: %w", err)92 return nil, fmt.Errorf("parsing identifier: %w", err)
51 }93 }
52 94
53- dir := identity.DefaultDirectory()95+ ident, err := dir().Lookup(ctx, atid)
54- ident, err := dir.Lookup(ctx, atid)
55 if err != nil {96 if err != nil {
56 return nil, fmt.Errorf("resolving identity: %w", err)97 return nil, fmt.Errorf("resolving identity: %w", err)
57 }98 }
modified main.go +3 -1
@@ -21,11 +21,13 @@ import (
2121 func main() {
2222 addr := flag.String("addr", envOr("GLEAN_ADDR", ":8080"), "listen address")
2323 dbPath := flag.String("db", envOr("GLEAN_DB", "glean.db"), "database path")
24- jetstreamURL := flag.String("jetstream", envOr("GLEAN_JETSTREAM", "wss://jetstream2.fr.hose.cam"), "Jetstream URL")
24+ jetstreamURL := flag.String("jetstream", envOr("GLEAN_JETSTREAM", "wss://jetstream.glean.at"), "Jetstream URL")
2525 syncInterval := flag.Duration("sync-interval", envDuration("GLEAN_SYNC_INTERVAL", 1*time.Hour), "PDS sync interval")
2626 clusterInterval := flag.Duration("cluster-interval", envDuration("GLEAN_CLUSTER_INTERVAL", 6*time.Hour), "cluster recomputation interval")
2727 flag.Parse()
2828
29+ atproto.InitIdentity(envOr("GLEAN_PLC_URL", ""))
30+
2931 logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
3032
3133 database, err := db.Open(*dbPath)
@@ -21,11 +21,13 @@ import (
21 func main() {21 func main() {
22 addr := flag.String("addr", envOr("GLEAN_ADDR", ":8080"), "listen address")22 addr := flag.String("addr", envOr("GLEAN_ADDR", ":8080"), "listen address")
23 dbPath := flag.String("db", envOr("GLEAN_DB", "glean.db"), "database path")23 dbPath := flag.String("db", envOr("GLEAN_DB", "glean.db"), "database path")
24- jetstreamURL := flag.String("jetstream", envOr("GLEAN_JETSTREAM", "wss://jetstream2.fr.hose.cam"), "Jetstream URL")24+ jetstreamURL := flag.String("jetstream", envOr("GLEAN_JETSTREAM", "wss://jetstream.glean.at"), "Jetstream URL")
25 syncInterval := flag.Duration("sync-interval", envDuration("GLEAN_SYNC_INTERVAL", 1*time.Hour), "PDS sync interval")25 syncInterval := flag.Duration("sync-interval", envDuration("GLEAN_SYNC_INTERVAL", 1*time.Hour), "PDS sync interval")
26 clusterInterval := flag.Duration("cluster-interval", envDuration("GLEAN_CLUSTER_INTERVAL", 6*time.Hour), "cluster recomputation interval")26 clusterInterval := flag.Duration("cluster-interval", envDuration("GLEAN_CLUSTER_INTERVAL", 6*time.Hour), "cluster recomputation interval")
27 flag.Parse()27 flag.Parse()
28 28
29+ atproto.InitIdentity(envOr("GLEAN_PLC_URL", ""))
30+
29 logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))31 logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
30 32
31 database, err := db.Open(*dbPath)33 database, err := db.Open(*dbPath)
modified readme.md +11 -9
@@ -10,6 +10,7 @@ Your subscriptions live as records on your PDS. You own them. If Glean goes away
1010
1111 - RSS, Atom, and JSON Feed support with a keyboard-driven reading interface
1212 - Highlights, notes, tags, and ratings on articles
13+- [margin.at](https://margin.at) annotations displayed alongside glean annotations
1314 - A trending page showing what's popular across all users
1415 - Feed and people recommendations based on reading overlap
1516 - OPML import and export
@@ -36,15 +37,16 @@ Then open `http://localhost:8080`.
3637
3738 ## Configuration
3839
39-| Variable | Default | What it does |
40-| -------------------------- | ------------------------------ | --------------------------------------------------------- |
41-| `GLEAN_ADDR` | `:8080` | Listen address |
42-| `GLEAN_DB` | `glean.db` | SQLite database path |
43-| `GLEAN_JETSTREAM` | `wss://jetstream2.fr.hose.cam` | Jetstream WebSocket URL |
44-| `GLEAN_SYNC_INTERVAL` | `1h` | PDS sync interval (Go duration: `30m`, `2h30m`, etc.) |
45-| `GLEAN_CLUSTER_INTERVAL` | `6h` | Cluster recomputation interval (Go duration) |
46-| `GLEAN_OAUTH_CLIENT_ID` | _(empty)_ | OAuth client metadata URL (leave empty for localhost dev) |
47-| `GLEAN_OAUTH_REDIRECT_URL` | _(empty)_ | OAuth redirect URL (leave empty for localhost dev) |
40+| Variable | Default | What it does |
41+| -------------------------- | -------------------------- | --------------------------------------------------------- |
42+| `GLEAN_ADDR` | `:8080` | Listen address |
43+| `GLEAN_DB` | `glean.db` | SQLite database path |
44+| `GLEAN_JETSTREAM` | `wss://jetstream.glean.at` | Jetstream WebSocket URL |
45+| `GLEAN_SYNC_INTERVAL` | `1h` | PDS sync interval (Go duration: `30m`, `2h30m`, etc.) |
46+| `GLEAN_CLUSTER_INTERVAL` | `6h` | Cluster recomputation interval (Go duration) |
47+| `GLEAN_PLC_URL` | `https://plc.directory` | PLC directory URL for DID resolution |
48+| `GLEAN_OAUTH_CLIENT_ID` | _(empty)_ | OAuth client metadata URL (leave empty for localhost dev) |
49+| `GLEAN_OAUTH_REDIRECT_URL` | _(empty)_ | OAuth redirect URL (leave empty for localhost dev) |
4850
4951 For production:
5052
@@ -10,6 +10,7 @@ Your subscriptions live as records on your PDS. You own them. If Glean goes away
10 10
11 - RSS, Atom, and JSON Feed support with a keyboard-driven reading interface11 - RSS, Atom, and JSON Feed support with a keyboard-driven reading interface
12 - Highlights, notes, tags, and ratings on articles12 - Highlights, notes, tags, and ratings on articles
13+- [margin.at](https://margin.at) annotations displayed alongside glean annotations
13 - A trending page showing what's popular across all users14 - A trending page showing what's popular across all users
14 - Feed and people recommendations based on reading overlap15 - Feed and people recommendations based on reading overlap
15 - OPML import and export16 - OPML import and export
@@ -36,15 +37,16 @@ Then open `http://localhost:8080`.
36 37
37 ## Configuration38 ## Configuration
38 39
39-| Variable | Default | What it does |40+| Variable | Default | What it does |
40-| -------------------------- | ------------------------------ | --------------------------------------------------------- |41+| -------------------------- | -------------------------- | --------------------------------------------------------- |
41-| `GLEAN_ADDR` | `:8080` | Listen address |42+| `GLEAN_ADDR` | `:8080` | Listen address |
42-| `GLEAN_DB` | `glean.db` | SQLite database path |43+| `GLEAN_DB` | `glean.db` | SQLite database path |
43-| `GLEAN_JETSTREAM` | `wss://jetstream2.fr.hose.cam` | Jetstream WebSocket URL |44+| `GLEAN_JETSTREAM` | `wss://jetstream.glean.at` | Jetstream WebSocket URL |
44-| `GLEAN_SYNC_INTERVAL` | `1h` | PDS sync interval (Go duration: `30m`, `2h30m`, etc.) |45+| `GLEAN_SYNC_INTERVAL` | `1h` | PDS sync interval (Go duration: `30m`, `2h30m`, etc.) |
45-| `GLEAN_CLUSTER_INTERVAL` | `6h` | Cluster recomputation interval (Go duration) |46+| `GLEAN_CLUSTER_INTERVAL` | `6h` | Cluster recomputation interval (Go duration) |
46-| `GLEAN_OAUTH_CLIENT_ID` | _(empty)_ | OAuth client metadata URL (leave empty for localhost dev) |47+| `GLEAN_PLC_URL` | `https://plc.directory` | PLC directory URL for DID resolution |
47-| `GLEAN_OAUTH_REDIRECT_URL` | _(empty)_ | OAuth redirect URL (leave empty for localhost dev) |48+| `GLEAN_OAUTH_CLIENT_ID` | _(empty)_ | OAuth client metadata URL (leave empty for localhost dev) |
49+| `GLEAN_OAUTH_REDIRECT_URL` | _(empty)_ | OAuth redirect URL (leave empty for localhost dev) |
48 50
49 For production:51 For production:
50 52