Repository navigation
Architecture
- Vite + React 19 + TypeScript
- React Router (client-side routing)
-
Vercel serverless functions (
api/) - Upstash Redis for server-side storage
- Anthropic SDK for the scheduling agent
- Google Identity Services + Google OAuth (sign-in + Calendar sync), Spotify OAuth (playback)
-
src/pages/— one component per route (Home,Matrix,Habits,Calendar,Friends,Pomodoro,Agent,School,Settings). Routing is defined insrc/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.tsxnests all of these providers around the router.FriendsContextsits insideCountdownsProvidersince it calls into it directly to keep a friend's birthday synced with a Countdowns entry.ThemeContextandNavVisibilityContextare local device preferences (localStorage) — everything else syncs throughremoteStore.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 byapi/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.
- The browser gets a Google ID token via Google Identity Services
(
VITE_GOOGLE_CLIENT_ID). - It's POSTed to
api/session.ts, which verifies the token with Google and checks the email againstALLOWED_EMAILS. - On success, the server issues an HMAC-signed session token (signed with
SESSION_SECRET, seeapi/_lib/session.ts) — a lightweight bearer token, not a full JWT. - The client stores it in
localStorageand sends it asAuthorization: Bearer <token>on everyapi/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.
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.
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/googleOAuthCallbackare preserved via rewrites invercel.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 byns(school,habits,matrix,countdowns,friends,agentChat,dailyPlan,watchdogDismissed), keyed per signed-in user. This is what most of the app's persistence goes through (viasrc/lib/remoteStore.tsand itsRemoteNamespaceunion) — adding a new synced feature means adding its namespace here, toRemoteNamespace, and toEXPORTABLE_NAMESPACESinsrc/lib/exportImport.tsso 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 theclassroom.coursework.me.readonlyscope), keyed by?action=. Merged with manually entered homework insrc/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).
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.
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.
FourFold · MIT License · Report an issue · Changelog (currently v1.4.1)