Skip to content

Milestones

List view

  • Goal: Replace 10s polling with push notifications for in-app “red dot” alerts and activity badges (borrow requests, approvals, returns, sticky-note updates), while keeping a graceful polling fallback. Background / Problem Today’s in-app messaging/alerts rely on a 10s poll to a notifications endpoint. At scale this creates unnecessary function invocations (Vercel) and latency, and it couples overall system load to background polling volume. Objectives Deliver realtime notifications to online users (sub-second typical). Reduce load from polling by ≥90% (keep 60s fallback only). Harden UX: offline queueing on reconnect, accurate unread counts. Keep scope small: “notes + nudges, not chat.” This is event fan-out, not a threaded messenger. Non-Goals Building a full chat system. Public/global chat rooms. Web Push to OS notification center (optional later). Architecture (Decision) Provider: Ably Channels (global WebSocket pub/sub). Browsers subscribe to capability-scoped channels. App servers publish on domain events. Channels: user:{userId} — direct notifications (red dot, requests to you) library:{libraryId} — branch activity (optional, used by admins or library activity views) site:announcements — rare, systemwide notes (maintenance) Message envelope (v1): { "v": 1, "type": "borrow.requested|borrow.approved|borrow.returned|note.updated|system.announcement", "entityId": "item_123", "libraryId": "lib_456", "actorId": "user_789", "at": "2025-09-01T00:00:00.000Z", "payload": { "title": "Jamie requested your drill", "dueAt": "2025-09-06T18:00:00Z" } } Auth: Next.js API route mints short-lived Ably token requests (1h). Capabilities restrict clients to subscribe only on their user and joined libraries. Server uses REST API key for publish. Fallback: if socket is down, poll GET /notifications/summary?since=ts every 60s to refresh unread counts. UX / Flows (MVP) Borrower submits request → publish borrow.requested to user:{ownerId}. Lender approves/declines → publish borrow.approved|borrow.declined to user:{borrowerId}. Sticky note added/edited on the transaction → publish note.updated to both parties. Borrower marks returned → publish borrow.returned to lender; on confirm → to borrower. Red-dot badge lights instantly; tapping opens the relevant transaction screen. Success Metrics 95th percentile delivery latency < 800ms (provider to client handler). Polling volume reduced ≥90%. Connection stability: < 2% sessions with >3 reconnects/hour. Event loss (publish → client ack) < 0.1% (sampled). Risks & Mitigations Dropped sockets / flaky networks: auto-reconnect with backoff; reconcile via 60s summary poll. Duplicate events: enforce idempotency via message.id + client de-dupe set. Auth leakage: capability-scoped tokens; never expose raw API keys. Over-notification: throttle non-critical events; collapse bursts (e.g., combine multiple item adds). Tasks (break into issues) Infra / Security Create Ably apps/keys for dev, staging, prod; store in Vercel env vars: ABLY_API_KEY, ABLY_ENV, ABLY_CLIENT_ID_PREFIX Rotate keys policy and playbook documented. Add feature flag FEATURE_REALTIME_NOTIFICATIONS (env-controlled). Backend (Next.js API / Services) Token endpoint: POST /api/realtime/token (auth required) Returns Ably token request scoped to user:{id} and joined libraries. TTL 1h; include clientId: user:{id}. Publisher module: lib/realtime/publish.ts publishUser(userId, type, payload, meta) publishLibrary(libraryId, type, payload, meta) Attaches envelope and ISO timestamp. Emit events at domain boundaries: on borrow request create (borrow.requested → owner) on approve (borrow.approved → borrower) on decline (borrow.declined → borrower) on sticky note update (note.updated → both) on return marked / confirmed (borrow.returned, borrow.confirmed → counterpart) Notifications summary endpoint (fallback): GET /api/notifications/summary?since=ISO Returns unread counts + last event timestamp. Server analytics: log publish success/failure + latency (StatsD/OTEL). Frontend (App) Realtime client: lib/realtime/client.ts Ably Realtime instance with authUrl: /api/realtime/token Auto-reconnect, exponential backoff, heartbeat. Subscription manager: Sub to user:{id} on login; sub to library:{id} for a user’s active libraries. Handle type → dispatch to UI store (Zustand/Redux). Maintain in-memory de-dupe by message.id. Badge/indicator: integrate with existing red-dot component; update counts on message receive. Fallback poller (only when socket down): 60s interval to /api/notifications/summary. Accessibility: ARIA live region for incoming notifications (“new activity” announcement). Admin & Ops Admin toggle in feature flags page to enable/disable realtime per environment. Dev tool: simple in-app event console (hidden route) to view last 50 events for the logged-in user. Slack webhook (optional): forward site:announcements or critical ops events. QA / Testing Unit tests for publisher (envelope shape, channel selection). Contract tests for token endpoint (capabilities correct). E2E test: simulate borrow request → owner sees red dot within 2s. Offline test: disconnect network, trigger events, reconnect; verify catch-up via summary. Load test small cohort: 200 concurrent sockets; measure reconnects & latency. Docs ADR: Realtime Provider Choice (why Ably, alternatives considered, rollback plan). Runbook: common errors, how to rotate keys, how to inspect channels. Developer guide: how to publish new event types (naming, payload). Code Skeletons Token endpoint (Next.js / Edge or Node runtime) // /app/api/realtime/token/route.ts (Next 13+) import { NextResponse } from 'next/server'; import Ably from 'ably/promises'; import { requireAuth, getUserLibraries } from '@/lib/authz'; export async function POST(req: Request) { const user = await requireAuth(req); // { id: 'u_123' } const libs = await getUserLibraries(user.id); // ['lib_1','lib_2'] const client = new Ably.Rest(process.env.ABLY_API_KEY!); const capability: Record<string, string[]> = { [`user:${user.id}`]: ['subscribe'], }; libs.forEach(id => capability[`library:${id}`] = ['subscribe']); const tokenRequest = await client.auth.createTokenRequest({ clientId: `user:${user.id}`, capability: JSON.stringify(capability), ttl: 60 * 60 * 1000, // 1h }); return NextResponse.json(tokenRequest); } Publisher // /lib/realtime/publish.ts import Ably from 'ably/promises'; const rest = new Ably.Rest(process.env.ABLY_API_KEY!); type EventType = | 'borrow.requested' | 'borrow.approved' | 'borrow.declined' | 'borrow.returned' | 'borrow.confirmed' | 'note.updated' | 'system.announcement'; export async function publishUser(userId: string, type: EventType, payload: any, meta?: any) { const channel = rest.channels.get(`user:${userId}`); const message = { id: `${type}:${payload?.transactionId ?? payload?.id}:${Date.now()}`, name: 'notification', data: { v:1, type, at: new Date().toISOString(), ...meta, payload }, }; await channel.publish(message.name, message.data, message.id); } export async function publishLibrary(libraryId: string, type: EventType, payload: any, meta?: any) { const ch = rest.channels.get(`library:${libraryId}`); const data = { v:1, type, at: new Date().toISOString(), ...meta, payload }; await ch.publish('notification', data); } Client bootstrap // /lib/realtime/client.ts import Ably from 'ably'; let r: Ably.Realtime | null = null; export function getRealtime() { if (r) return r; r = new Ably.Realtime.Promise({ authUrl: '/api/realtime/token', recover: (lastConnectionDetails, cb) => cb(true), // try to resume }); return r; } Subscribe on login // /lib/realtime/subscribe.ts import { getRealtime } from './client'; export function subscribeUser(userId: string, libraryIds: string[], onMsg: (evt:any)=>void) { const rt = getRealtime(); const userCh = rt.channels.get(`user:${userId}`); userCh.subscribe('notification', (msg) => onMsg(msg.data)); libraryIds.forEach(id => { const ch = rt.channels.get(`library:${id}`); ch.subscribe('notification', (msg) => onMsg(msg.data)); }); } Rollout Plan Dev only behind flag → verify flows. Staging cohort (internal + 5–10 trusted testers). Prod canary: enable realtime for 5% of users; keep 60s fallback for all. Expand to 100%, then reduce fallback poll to 120s; eventually disable polling if metrics stay green. Definition of Done Flag on in prod for 100% of beta users. Red-dot latency p95 < 800ms in canary and full rollout. Polling traffic reduced ≥90% vs. baseline. Runbook + ADR merged; monitoring dashboards linked in README. Open Questions Do we want web push for offline nudges in v1.1? Should library-level activity be subscribed only when viewing that library to save connections? Retention: do we persist notifications server-side for a 7-day inbox view, or just summary counts?

    No due date
  • Getting eyes and ears on stuff libararies everywhere, and super user tools to help manage things responsibly

    No due date
    6/10 issues closed
  • Users can check stuff out and check it back in

    Overdue by 4 month(s)
    Due by August 29, 2025
    9/10 issues closed
  • A user can sign up, verify identity, upload photo/video, and receive a digital library card. They can log out, log back in, and see their persistent card/profile.

    Overdue by 5 month(s)
    Due by August 18, 2025
    6/7 issues closed
  • Build a lightweight, trustworthy landing page that sets tone, captures emails, and showcases the library metaphor (cards, branches). Includes brand tokens, hero, explainer, video placeholder, and footer.

    No due date
    11/11 issues closed
  • Implement the core membership model: branches represent neighborhood groups, and library cards represent patron membership. Patrons can create accounts, open new branches, and invite others. Early emphasis is on safe, trust-building flows—invite QR codes, postcard invites, and geofencing for branch joins. The UX should lean heavily into the library metaphor, with digital library cards and item checkout cards styled after classic stamped cards. Completion means a new user can create a profile, open a branch, and issue/accept a library card invite.

    No due date
  • Set up the foundation of the project: repo structure, CI/CD, testing, and initial scaffolding. The goal is to make development smooth for humans and AIs, with automated linting, tests, Playwright integration, and a simple homepage that signals trust and legitimacy. Completion means contributors can run the project locally, CI runs on every PR, and a “Coming Soon” site is live with privacy/terms placeholders.

    Overdue by 4 month(s)
    Due by August 31, 2025
    20/20 issues closed