···20202121**data flow:**
22222323-- `+page.server.ts` — SSR only. calls `com.atproto.sync.listRepos` (unauthenticated, max 1000), then `Promise.allSettled` over all active DIDs doing parallel `describeRepo` + `getRecord` fetches. avatar URLs use `https://blobs.blue/{did}/avatar@webp` (no CID parsing needed).
2323+- `+page.server.ts` — SSR only. calls `com.atproto.sync.listRepos` (unauthenticated, paginated) filtering out deactivated/takendown repos, then `Promise.allSettled` over remaining DIDs doing parallel `describeRepo` + `getRecord` fetches. avatar URLs use the bsky cdn format (`{CDN_URL}/img/avatar/plain/{did}/{cid}@jpeg`).
2424- `api/invite/+server.ts` — POST endpoint. in-memory IP rate limit (`Map<string, number[]>`), then raw `fetch` to the PDS using Basic auth (`admin:{PDS_ADMIN_PASSWORD}`). rate limiting is disabled in `dev` mode.
2525- `+page.svelte` — client component. invite code generation, clipboard copy, error display. pdsmoover link at bottom.
26262727-**env vars** (all read via `$env/dynamic/private` — resolved at runtime, never baked into bundles):
2727+**env vars** — all centralized in `$lib/server/env.ts` (read via `$env/dynamic/private`, resolved at runtime, never baked into bundles). see that file for defaults and docs.
28282929-- `PDS_URL` — required for all PDS calls
3030-- `PDS_ADMIN_PASSWORD` — required for invite code generation
3131-- `INVITE_RATE_LIMIT` — codes per IP per window (default 3)
3232-- `INVITE_RATE_WINDOW` — window in seconds (default 86400)
3333-3434-**key constraint:** `$lib/server/` is enforced server-only by SvelteKit. never use `process.env` directly for secrets — use `$env/dynamic/private`. never import from `$lib/server/` in `+page.svelte` or any client-accessible module.
2929+**key constraint:** `$lib/server/` is enforced server-only by SvelteKit. never use `process.env` directly for secrets — import from `$lib/server/env`. never import from `$lib/server/` in `+page.svelte` or any client-accessible module.
35303631**styling:** ported from `../site/` — Host Grotesk variable font, oklch color vars (`--color-bg`, `--color-surface`, `--color-fg`), dark mode via `prefers-color-scheme`. pds.ls button icon uses CSS `mask-image` over a vectorized SVG (`static/pdsls-icon.svg`, generated with potrace from the upstream PNG).
+16
src/lib/server/env.ts
···11+import { env } from '$env/dynamic/private';
22+33+// PDS base URL for all atproto API calls
44+export const PDS_URL = env.PDS_URL ?? 'https://pds.madoka.systems';
55+66+// PDS admin password for privileged operations (invite code generation)
77+export const PDS_ADMIN_PASSWORD = env.PDS_ADMIN_PASSWORD;
88+99+// bsky-compatible image CDN base URL for avatar proxying
1010+export const CDN_URL = env.CDN_URL ?? 'https://cdn.madoka.systems';
1111+1212+// max invite codes a single IP can generate per window
1313+export const INVITE_RATE_LIMIT = parseInt(env.INVITE_RATE_LIMIT ?? '3', 10) || 3;
1414+1515+// rate limit window in ms (env var is in seconds, default 86400 = 24h)
1616+export const INVITE_RATE_WINDOW = (parseInt(env.INVITE_RATE_WINDOW ?? '86400', 10) || 86400) * 1000;
+1-3
src/lib/server/pds.ts
···11import { Client, simpleFetchHandler } from '@atcute/client';
22-import { env } from '$env/dynamic/private';
33-44-const PDS_URL = env.PDS_URL ?? 'https://pds.madoka.systems';
22+import { PDS_URL } from './env';
5364export const publicRpc = new Client({
75 handler: simpleFetchHandler({ service: PDS_URL }),