Atmo Notifs — website #
The user-facing dashboard for the atproto notifications relay. Users sign in with Bluesky (atproto OAuth), approve/revoke which apps may notify them, link Telegram, and manage settings. Built with SvelteKit + Tailwind v4.
Names are placeholders. The product name (
PROJECT_NAME) and footer repo link (GITHUB_URL) live insrc/lib/config.ts. Relay constants (domain, DID, scope) are in the same file.
How it fits together #
- Auth — provided by
@svelte-atproto/oauth(scaffolded; do not modifysrc/hooks.server.ts,src/app.d.ts, orsrc/lib/atproto/index.tsbeyond the OAuth scope). The hook populateslocals.did/locals.session/locals.client. - Reads — page
loadfunctions (+page.server.ts) call the relay in parallel. - Writes — SvelteKit remote functions (
commands insrc/lib/remote/notifs.remote.ts); the page callsinvalidateAll()after each. - Relay calls —
src/lib/server/relay.tsmints a per-request service-auth JWT via the user's PDS (com.atproto.server.getServiceAuth) and calls the relay. The JWT never reaches the browser. - Types — request/response shapes come from the workspace package
@atmo/notifs-lexicons.
Remote functions are enabled via kit.experimental.remoteFunctions +
compilerOptions.experimental.async in svelte.config.js.
Routes #
| Route | Purpose |
|---|---|
/ |
Landing + "Sign in with Bluesky" |
/dashboard |
Pending requests, granted apps, channels, settings |
/dashboard/pending/[id] |
Deep-linkable single pending request (bot links here) |
/docs |
Developer docs for sending notifications |
Theming #
Light/dark via semantic CSS variables in src/routes/layout.css (exposed to
Tailwind through @theme inline, so bg-surface, text-muted, etc. switch
automatically). Defaults to the OS preference; the header toggle overrides it
(persisted in localStorage, applied before paint by a small script in
src/app.html to avoid a flash).
Local development #
pnpm install
pnpm atproto:setup # generates dev OAuth keys / .env (from @svelte-atproto)
pnpm dev
Set the env vars in .env (see .env.example): ORIGIN, COOKIE_SECRET,
CLIENT_ASSERTION_KEY. The relay must be reachable at the domain in
src/lib/config.ts (relay.atmo.pub) for dashboard data to load — point it at
a local relay during development if needed.
pnpm check # svelte-check (type check)
pnpm build # production build
Deployment (Cloudflare Workers) #
Uses @sveltejs/adapter-cloudflare (Workers + static assets). Run these from
apps/web/ in a Cloudflare-authenticated terminal.
- Pick the domain. In
wrangler.jsonc, setnameandvars.ORIGIN(e.g.https://atmo.pub).ORIGINmust equal the served URL — the atproto OAuthclient_idis derived from it. - Create the KV namespaces and paste the ids into
wrangler.jsonc:pnpm exec wrangler kv namespace create OAUTH_SESSIONS pnpm exec wrangler kv namespace create OAUTH_STATES - Set the secrets (the
atproto-oauthCLI generates them):pnpm exec atproto-oauth secret | pnpm exec wrangler secret put COOKIE_SECRET pnpm exec atproto-oauth keygen | pnpm exec wrangler secret put CLIENT_ASSERTION_KEY - Deploy (builds, then
wrangler deploy):pnpm run deploy - Bind the custom domain in the CF dashboard (Workers → the worker →
Settings → Domains & Routes), matching
ORIGIN.
The OAuth client metadata is served dynamically by @svelte-atproto/oauth — no
static file to write. The dashboard needs the relay reachable at the domain in
src/lib/config.ts (relay.atmo.pub), so deploy the relay too.