Skip to content

Architecture

Krennix edited this page Sep 21, 2026 · 3 revisions

Architecture

Tech stack

Frontend structure

  • src/pages/ — one component per route (Home, Matrix, Habits, Calendar, Friends, Pomodoro, Agent, School, Settings). Routing is defined in src/App.tsx.
  • src/state/ — React Context providers, one per feature area (SchoolContext, SchoologyContext, CanvasContext, ClassroomContext, HabitsContext, MatrixContext, CalendarContext, CountdownsContext, FriendsContext, PomodoroContext, AgentContext, NotificationsContext, GoogleAuthContext, GoogleIcsContext, SpotifyAuthContext, AuthContext, ThemeContext, NavVisibilityContext). App.tsx nests all of these providers around the router. FriendsContext sits inside CountdownsProvider since it calls into it directly to keep a friend's birthday synced with a Countdowns entry. ThemeContext and NavVisibilityContext are local device preferences (localStorage) — everything else syncs through remoteStore.ts/api/data.ts.
  • src/components/ — shared UI (Sidebar, dialogs, widgets, the login gate, context menu, mention field, etc.).
  • src/lib/ — non-React logic: remoteStore.ts (persistence helper backed by api/data.ts), scheduling.ts/useAutoSchedule.ts (auto-scheduling logic used by the agent and matrix/habits), googleCalendar.ts, spotify.ts, harkerBell.ts (school-specific bell schedule client), homeworkMerge.ts, urgency.ts.

Auth model

  1. The browser gets a Google ID token via Google Identity Services (VITE_GOOGLE_CLIENT_ID).
  2. It's POSTed to api/session.ts, which verifies the token with Google and checks the email against ALLOWED_EMAILS.
  3. On success, the server issues an HMAC-signed session token (signed with SESSION_SECRET, see api/_lib/session.ts) — a lightweight bearer token, not a full JWT.
  4. The client stores it in localStorage and sends it as Authorization: Bearer <token> on every api/ call.

There's an optional fallback path for devices where the Google popup isn't convenient: api/pinSession.ts checks an email against ALLOWED_EMAILS and a shared passcode against LOGIN_PIN (constant-time compare), then mints a session the same way. It stores the literal email the user typed (not the alias-normalized form used for the allowlist check) so a session started this way lands on the same per-user data key as one from Google. Unset LOGIN_PIN and the fallback is disabled — Google stays the only way in.

This is a single-tenant, allowlist-gated model — there's no self-serve signup. It's meant for one person (or a small trusted group) running their own deployment, not a multi-tenant SaaS.

Google/Spotify Calendar sync is separate from sign-in

Signing in (step above) only proves who you are. To actually sync Google Calendar or Spotify, the app runs a second, independent OAuth flow (api/google.ts / api/spotify.ts) that requests Calendar/Spotify scopes and stores a refresh token server-side in Redis, keyed by fourfold:<app email>:.... This lets Calendar/Spotify sync survive across devices and browser sessions, and means the app — not just the browser — can hold a live token.

api/ layout

Vercel's Hobby plan caps the number of serverless functions per deployment, so related endpoints are consolidated into single files, routed internally by an ?action= query param:

  • api/google.ts — Google OAuth start/callback, access token minting, linked-calendar management, and the ICS backup feed. External URLs like /api/googleOAuthCallback are preserved via rewrites in vercel.json (e.g. /api/googleOAuthCallback → /api/google?action=oauthCallback) — this is what you register as the redirect URI with Google, not the internal path.
  • api/spotify.ts — same pattern for Spotify OAuth + account info.
  • api/session.ts — Google ID token verification + session issuance.
  • api/pinSession.ts — the email+PIN fallback sign-in (see Auth model above).
  • api/data.ts — a small generic key/value store over Redis, namespaced by ns (school, habits, matrix, countdowns, friends, agentChat, dailyPlan, watchdogDismissed), keyed per signed-in user. This is what most of the app's persistence goes through (via src/lib/remoteStore.ts and its RemoteNamespace union) — adding a new synced feature means adding its namespace here, to RemoteNamespace, and to EXPORTABLE_NAMESPACES in src/lib/exportImport.ts so it round-trips through export/import too.
  • api/agent.ts — the Anthropic-powered scheduling agent: takes the current schedule/tasks/habits as context and lets the model call tools to read/propose changes.
  • api/lms.ts — fetches and merges assignments from Schoology (ICS feed and/or its personal API for points/full descriptions), Canvas (personal access token), and Google Classroom (reuses the Google OAuth client with the classroom.coursework.me.readonly scope), keyed by ?action=. Merged with manually entered homework in src/lib/homeworkMerge.ts.
  • api/_lib/ — shared helpers: googleOAuth.ts, googleTokens.ts, spotifyOAuth.ts, spotifyTokens.ts, session.ts, ics.ts, scheduling.ts, canvasApi.ts, classroomApi.ts, schoologyApi.ts, lmsShared.ts, apiTokens.ts (personal API tokens for reading merged homework outside the app), oauth1.ts (Schoology's OAuth1 personal API).

Storage

Everything server-side is stored in Upstash Redis as simple key/value pairs, namespaced as fourfold:<email>:<thing> (e.g. fourfold:you@gmail.com:googleCalendars, fourfold:you@gmail.com:schoology). There's no relational database or schema migrations — just JSON blobs per user per feature.

School-specific integration

src/lib/harkerBell.ts talks to a public bell-schedule API for a specific middle school (Harker). It's used by the School page to show class period times. If your school doesn't have an equivalent public API, this is the piece to swap out or remove — it's isolated to that one file plus its usage in src/pages/School.tsx.

Clone this wiki locally