notification manager for bsky noti.waow.tech
0

Configure Feed

Select the types of activity you want to include in your feed.

128 3 0

Clone this repository

https://git.vm.fail/zzstoatzz.io/noti https://git.vm.fail/did:plc:4mliscnpi5wd7eq3c42hwqqj
ssh://git@knot1.tangled.sh:2222/zzstoatzz.io/noti ssh://git@knot1.tangled.sh:2222/did:plc:4mliscnpi5wd7eq3c42hwqqj

For self-hosted knots, clone URLs may differ based on your setup.


README.md

noti#

noti pools notifications from a linked group of ATProto accounts and recommends account-scoped notification actions from recent inbox state and preferences.

It logs in with atproto OAuth, reads the live Bluesky notification inbox, and can:

  • mute or unmute an account
  • block or unblock an account
  • turn post notifications on or off for an account
  • turn reply notifications on or off for an account
  • mark notifications seen

One allowlisted account owns a persistent account group. Additional accounts can be linked with their own OAuth grants, switched in the UI, and observed together in a shared unread-notification pool. The cursor API retains the full observed event history. Mutations always remain scoped to one active account.

Mutations are code-mode: model-written compositions over a restricted atproto SDK surface, executed in a Cloudflare Worker isolate and verified against live state before success is reported.

architecture#

  • Cloudflare Worker app in src/worker.ts
  • D1-backed OAuth state, persistent account groups, browser sessions, and user guidance in src/cloudflare/*
  • One UserGraph Durable Object per account DID for account-local history and features
  • One GroupInbox Durable Object per account group for an ordered, replayable notification event log
  • Worker Loader sandboxing in src/cloudflare/code-mode.ts
  • Background recommendations via Cloudflare Queues with D1 job/result storage
  • At-least-once outbound delivery via a dedicated Cloudflare Queue, with encrypted destination credentials and a D1 delivery ledger
  • D1 allowlist and LLM usage tracking/limits in src/cloudflare/usage.ts
  • Anthropic prompt caching on the stable code-mode system prompt
  • Local app bundle from scripts/build-client.ts, including pinned htmx.org and src/client.ts
  • Login typeahead backed by https://typeahead.waow.tech

shared notification API#

The browser-authenticated event surface is:

GET /api/groups/current/notifications?after=0&limit=100&account=did:plc:...
  • Omit after to receive the latest events in newest-first order.
  • Send after=0 to replay from the oldest retained event, or pass a returned cursor to receive newer events in sequence order.
  • account is optional and must identify an account linked to the current group.
  • Events are idempotent by (account DID, notification URI) and retain a stable group sequence.

The five-minute cron syncs the notification bus for linked accounts. Every fifteen minutes it also performs the full account snapshot and recommendation refresh. Initial linking backfills up to 500 recent notifications; subsequent polls stop when they reach an event already present in the group log.

outbound delivery#

Account-group owners can connect a Discord webhook from settings. noti validates the destination against Discord's webhook URL shape, sends a test message before saving it, and encrypts the credential with AES-GCM. A strict connection/resume watermark rejects notifications indexed before the integration became active, even if an inbox backfill first observes them later.

The default delivery strategy includes direct interactions (mentions, replies, and quotes). Owners can independently enable follows, subscribed activity, and noisy likes/reposts. Each eligible shared-pool sequence becomes one idempotent D1 delivery record. A dedicated, single-consumer Cloudflare Queue performs at-least-once delivery with bounded retries and groups bursts into Discord messages of up to ten events; permanent failures remain visible in the integration status. Discord is the first provider adapter—the shared event log and delivery queue are provider-independent.

OAuth scopes#

noti requests granular atproto OAuth scopes:

  • atproto
  • repo:app.bsky.graph.block
  • rpc:app.bsky.notification.listNotifications?aud=*
  • rpc:app.bsky.notification.listActivitySubscriptions?aud=*
  • rpc:app.bsky.notification.putActivitySubscription?aud=*
  • rpc:app.bsky.notification.updateSeen?aud=*
  • rpc:app.bsky.graph.getMutes?aud=*
  • rpc:app.bsky.graph.getBlocks?aud=*
  • rpc:app.bsky.graph.muteActor?aud=*
  • rpc:app.bsky.graph.unmuteActor?aud=*
  • rpc:app.bsky.actor.getProfile?aud=*
  • rpc:app.bsky.actor.getProfiles?aud=*
  • rpc:app.bsky.feed.getPosts?aud=*

local dev#

Create .dev.vars:

ANTHROPIC_API_KEY=...
WEBHOOK_ENCRYPTION_KEY=... # 32 random bytes encoded as base64url

Generate the webhook encryption key with:

bun -e 'console.log(Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url"))'

Run:

bun install
bunx wrangler d1 migrations apply noti --local
bun run dev -- --ip 127.0.0.1 --port 8787

Open http://127.0.0.1:8787/oauth/login.

Allow a DID locally:

bunx wrangler d1 execute noti --local --command "INSERT INTO invite_allowlist (user_did, note, created_at) VALUES ('did:plc:...', 'local dev', datetime('now'))"

checks#

bun run check
bun test
bun run lint
bun run check:dry-run

deploy#

Before deploying:

  • set ANTHROPIC_API_KEY as a Cloudflare secret
  • create the noti-recommendations Cloudflare Queue
  • apply D1 migrations to noti
  • add allowed user DIDs to invite_allowlist

Then:

bun run deploy

Current deployment: https://noti.n8-3e9.workers.dev