A full-stack flashcard and study platform built for students. Riven combines spaced-repetition study tools with class management, LMS integration, AI-powered card generation, social features, and a deeply customizable botanical aesthetic — all wrapped in a PWA with native iOS support.
- Key Features
- Tech Stack
- Prerequisites
- Getting Started
- Architecture
- Supabase Edge Functions
- Environment Variables
- Available Scripts
- Testing
- Deployment
- Troubleshooting
- Deck & Card Management — Folders, tags, image-based cards (front/back), deck sharing via unique codes.
- Study Modes — Standard study, test mode, and group cram sessions with real-time sync.
- Class & Assignment Tracker — Weekly timetable, assignments with Canvas/iCal sync.
- AI Flashcard Generation — Generate cards from text or document uploads via Google Gemini.
- Social — Friend system, direct messaging (text, images, deck sharing), blocking, and reporting.
- Study Groups — Shared decks, file folders, and live collaborative cram sessions.
- Gamification — Daily streaks, virtual garden/pet system with accessories.
- Monetization — Free; Supporter monthly ($5.99/mo) and annual ($74.99/yr) via Stripe. Promotional or complimentary lifetime access also exists (referrals, admin). Hearts system for free-tier gating.
- Referral Program — Invite friends via referral codes, track qualified signups.
- Full Theming — User-defined color palettes, font pairs, and multiple saved themes.
- Security — 2FA (TOTP), email verification, password reset, XSS sanitization.
- Role-Based Access —
user,admin,ownerroles with admin panel for user management, analytics, and announcements. - PWA + iOS — Offline-capable PWA with Capacitor wrapper for native iOS builds.
| Layer | Technology |
|---|---|
| Runtime | Node.js 20+ |
| Frontend | React 19, Vite 7, React Router 7, Tailwind CSS 3, Motion (Framer), GSAP, Lucide React |
| Backend | Express 5 (API server) + Supabase Edge Functions (Deno) |
| Database | PostgreSQL via Supabase |
| Auth | Supabase Auth + JWT, Google OAuth, Apple Sign-In, 2FA (Speakeasy TOTP) |
| AI | Google Gemini (@google/genai) |
| Payments | Stripe (subscriptions + one-time purchases + webhooks) |
| Resend (password resets, email verification) | |
| Real-time | Socket.IO (DMs, group cram sessions) |
| LMS Sync | Canvas API, iCal parsing (node-ical) |
| File Handling | Mammoth (DOCX), react-pdf, docx-preview |
| Offline | IndexedDB (idb), Vite PWA Plugin |
| Testing | Vitest + Testing Library (client), Vitest + Supertest (server) |
| Mobile | Capacitor 8 (iOS native wrapper) |
| Deployment | Vercel (monorepo) + Supabase (DB + edge functions) |
- Node.js 20+
- npm (ships with Node)
- Supabase CLI — for local edge function development and deployment
npm install -g supabase
- PostgreSQL 15+ — via Supabase (cloud) or local install for development
git clone <repo-url>
cd riven
# Root dependencies (concurrently)
npm install
# Server dependencies
cd server && npm install && cd ..
# Client dependencies
cd client && npm install && cd ..cp server/.env.example server/.env| Variable | Required | Description | Example / How to Get |
|---|---|---|---|
DATABASE_URL |
Yes | Supabase PostgreSQL connection string | postgresql://postgres.[ref]:[password]@aws-0-us-west-2.pooler.supabase.com:6543/postgres |
JWT_SECRET |
Yes | Token signing key | openssl rand -base64 32 |
SUPABASE_URL |
No | Supabase project URL | https://[ref].supabase.co |
SUPABASE_SERVICE_ROLE_KEY |
No | Service role key for privileged operations | Supabase dashboard → Settings → API |
GEMINI_API_KEY |
No | Google Gemini for AI card generation | Google AI Studio |
FRONTEND_URL |
No | Used for OAuth redirects | http://localhost:5173 |
NODE_ENV |
No | development or production |
development |
PORT |
No | API port | 3000 |
ALLOWED_ORIGINS |
No | CORS origins (comma-separated) | http://localhost:5173,http://localhost:3000 |
STRIPE_SECRET_KEY |
No | Stripe server key | Stripe dashboard |
STRIPE_WEBHOOK_SECRET |
No | Stripe webhook signing secret | Stripe CLI / dashboard |
RESEND_API_KEY |
No | Resend email API key | Resend |
GOOGLE_CLIENT_ID |
No | Legacy Google ID-token verification for the compatibility bridge | GCP Console |
APPLE_CLIENT_ID |
No | Apple Sign-In service ID | Apple Developer |
APPLE_TEAM_ID |
No | Apple team identifier | Apple Developer |
APPLE_KEY_ID |
No | Apple private key ID | Apple Developer |
APPLE_PRIVATE_KEY |
No | Apple private key (PEM) | Apple Developer |
cp client/.env.example client/.env| Variable | Required | Description | Default (Dev) |
|---|---|---|---|
VITE_API_URL |
No | Backend API URL. Leave blank for local dev (Vite proxies /api). |
— |
VITE_STRIPE_PRICE_MONTHLY |
No | Stripe Price ID for monthly Supporter | — |
VITE_STRIPE_PRICE_ANNUAL |
No | Stripe Price ID for yearly Supporter (falls back to VITE_STRIPE_PRICE_LIFETIME if unset) |
— |
Google Sign-In for web/PWA is configured in Supabase Auth, not in client/.env.
Configure it in two places:
-
Google Cloud Console → OAuth client → Authorized redirect URIs
https://ghmnsmjjpdbpnrohjyrg.supabase.co/auth/v1/callback- This must match the active Supabase project ref exactly. If the project ref changes, update this callback URI in Google Cloud before testing sign-in.
- Do not add app routes like
/accountto Google Cloud redirect URIs.
-
Supabase Auth → URL Configuration / Redirect allowlist
- Canonical Site URL:
https://riven.rocks - Allowed redirect URLs:
https://riven.rocks/accounthttp://localhost:5173/accounthttp://localhost:3000/account
- App return URLs such as
/accountbelong in Supabase Auth, not in Google Cloud.
- Canonical Site URL:
For local/self-hosted Supabase, mirror the commented Google provider block in supabase/config.toml and provide SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID and SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET as environment variables.
Riven uses Supabase-hosted PostgreSQL. The server auto-creates all tables on startup via server/db.js — no manual migrations needed for initial setup.
For a fresh Supabase project:
- Create a project at supabase.com
- Copy the connection string from Settings → Database → Connection string (URI)
- Set it as
DATABASE_URLinserver/.env - Start the server — it handles schema creation, column migrations, index creation, and role seeding automatically
SQL migrations in supabase/migrations/ handle additional schema changes (RLS policies, realtime config, deduplication).
# From the project root — runs both client + server concurrently
npm startThis launches:
- Server:
http://localhost:3000(Express with nodemon auto-reload) - Client:
http://localhost:5173(Vite dev server, proxies/apito server)
Or run them individually:
npm run server # server only (nodemon)
npm run client # client only (vite)riven/
├── client/ # React Frontend (Vite)
│ ├── src/
│ │ ├── api.js # Hybrid API module (server + IndexedDB offline fallback)
│ │ ├── api/ # API client sub-modules (authApi, etc.)
│ │ ├── components/ # Reusable UI components
│ │ │ ├── auth/ # Login, Register, OAuth, 2FA modals
│ │ │ ├── layout/ # Sidebar, RootLayout
│ │ │ └── ui/ # Garden, Hearts, Pricing, PageLoader, etc.
│ │ ├── context/ # React contexts (Auth, Garden, Streak, Theme, UI)
│ │ ├── hooks/ # Custom hooks (useAuth, useStreak, useTheme, etc.)
│ │ ├── pages/ # Route-level page components
│ │ │ ├── Home.jsx # Landing (unauth) / Dashboard (auth)
│ │ │ ├── Decks.jsx # Deck list with folder/tag filtering
│ │ │ ├── DeckView.jsx # Single deck view + card editor
│ │ │ ├── CreateDeck.jsx # Manual, AI generate, or document import
│ │ │ ├── StudyMode.jsx # Flashcard study session
│ │ │ ├── TestMode.jsx # Quiz/test mode
│ │ │ ├── Classes.jsx # Class management + schedule
│ │ │ ├── ClassView.jsx # Single class: assignments, linked decks
│ │ │ ├── StudyGroups.jsx # Group list + creation
│ │ │ ├── GroupDetails.jsx # Group decks, files, members
│ │ │ ├── GroupCram.jsx # Live collaborative cram session
│ │ │ ├── Friends.jsx # Friend list + requests
│ │ │ ├── Messages.jsx # Direct messaging
│ │ │ ├── Settings.jsx # App settings, LMS integrations
│ │ │ ├── ThemeSettings.jsx # Theme editor + saved themes
│ │ │ ├── AdminPanel.jsx # Admin: user mgmt, analytics, announcements
│ │ │ └── ... # Account, EditProfile, UserProfile, GardenSettings, etc.
│ │ ├── routes/ # React Router route definitions
│ │ ├── db/ # IndexedDB helpers (offline storage)
│ │ ├── utils/ # Utility functions + animation helpers
│ │ ├── App.jsx # Root app component
│ │ ├── main.jsx # Entry point
│ │ └── index.css # Global styles + Tailwind base
│ ├── public/ # Static assets (icons, manifest, sounds)
│ ├── ios/ # Capacitor iOS project
│ ├── capacitor.config.json # Capacitor config
│ ├── vite.config.js # Vite config (proxy, PWA plugin)
│ └── tailwind.config.js # Tailwind config (custom theme tokens)
│
├── server/ # Express Backend
│ ├── index.js # Main entry: middleware, rate limiting, all route registration
│ ├── db.js # PostgreSQL pool + auto-schema init
│ ├── routes/ # Modular route files
│ │ ├── auth.js # Register, login, OAuth, 2FA, password reset, email verify
│ │ ├── admin.js # Admin panel (user mgmt, analytics, announcements, bans)
│ │ ├── ai.js # AI card generation (Gemini), document parsing (Mammoth)
│ │ ├── classes.js # CRUD for classes
│ │ ├── assignments.js # CRUD for assignments
│ │ ├── schedule.js # Weekly schedule slots
│ │ ├── groups.js # Study groups, members, shared decks, files, cram sessions
│ │ ├── social.js # Friends, DMs, blocking, reporting
│ │ ├── hearts.js # Hearts system (free-tier gating, refills, rewards)
│ │ ├── lms.js # Canvas API + iCal sync
│ │ ├── stripe.js # Checkout sessions, portal, subscription management
│ │ ├── webhooks.js # Stripe webhook handler (idempotent)
│ │ ├── referrals.js # Referral code generation, tracking, qualification
│ │ └── health.js # Health check endpoint
│ ├── utils/
│ │ └── email.js # Resend email helper
│ └── test/ # Backend tests (Vitest + Supertest)
│
├── supabase/ # Supabase Configuration
│ ├── config.toml # Supabase project config
│ ├── functions/ # Deno edge functions (18 functions + shared helpers)
│ │ ├── _shared/ # Shared helpers (auth, stripe, email, http)
│ │ ├── complete-registration/ # Post-signup user creation
│ │ ├── stripe-webhook/ # Stripe payment events
│ │ ├── forgot-password/ # Password reset initiation
│ │ ├── verify-email/ # Email verification
│ │ ├── reset-password/ # Complete password reset
│ │ ├── create-checkout/ # Stripe checkout session creation
│ │ ├── create-portal/ # Stripe billing portal
│ │ ├── canvas-lms/ # Canvas LMS integration
│ │ ├── generate-deck/ # AI deck generation
│ │ ├── generate-class/ # AI class generation
│ │ ├── ai-limits/ # AI generation rate limits
│ │ ├── hearts/ # Hearts system logic
│ │ ├── group-actions/ # Study group CRUD
│ │ ├── group-sessions/ # Group cram sessions
│ │ ├── account-actions/ # Account management
│ │ ├── admin-actions/ # Admin operations
│ │ ├── referrals/ # Referral tracking
│ │ └── accept-shared-deck/ # Shared deck acceptance
│ └── migrations/ # SQL migrations (RLS policies, realtime, deduplication)
│
├── riven-social/ # Standalone social landing page (separate Vite/TS app)
├── package.json # Root scripts (concurrently runs client + server)
├── vercel.json # Vercel monorepo deployment config
└── .gitignore
Riven uses a hybrid backend — requests are handled by either the Express server or Supabase edge functions depending on the operation:
Express Server Path (traditional API):
User Action → React UI → api.js fetch → /api/* → Express Middleware → Route Handler → PostgreSQL → JSON Response
Edge Function Path (auth, payments, AI, groups):
User Action → React UI → Supabase client → Edge Function → PostgreSQL / Stripe / Gemini → JSON Response
Offline Path (PWA fallback):
User Action → React UI → api.js → IndexedDB (local) → Cached Response
- Register / Login → Supabase Auth handles email/password or OAuth (Google/Apple)
- Post-Signup →
complete-registrationedge function creates the user record in theuserstable and linkssupabase_auth_id - JWT → Supabase issues JWT stored in session; server validates via
authMiddleware - 2FA → TOTP setup via Speakeasy, QR code via
qrcodelibrary. Enforced on login if enabled. - Protected Routes →
ProtectedRoutecomponent checks auth context on the client;authMiddlewarevalidates JWT on the server
React Context API for global state:
| Context | Purpose |
|---|---|
AuthContext |
User auth state, login/logout |
GardenContext |
Pet/streak garden state |
StreakContext |
Daily streak tracking |
ThemeContext |
User-defined theme (colors, fonts) |
UIContext |
Global UI state (modals, sidebar) |
ToastContext |
Toast notifications |
The full schema is defined in server/db.js and auto-creates on startup. Additional migrations live in supabase/migrations/.
| Table | Purpose |
|---|---|
users |
Profiles, auth, subscription tier, streak data, pet customization, 2FA, Stripe IDs, LMS tokens, referral codes, supabase_auth_id |
decks / cards |
Flashcard decks and cards with spaced repetition fields (difficulty, next_review) |
folders / tags / deck_tags |
Organizational folders and tagging system |
study_sessions |
Study/test session analytics (cards studied, accuracy, duration) |
classes / assignments / schedule_slots |
Course management with LMS sync |
study_groups / group_members / group_decks |
Study groups with shared content |
group_folders / group_files |
File sharing within groups |
cram_sessions / cram_responses |
Live collaborative study sessions |
themes |
User-created color themes with font pair customization |
shared_decks |
Publicly shared deck snapshots |
friendships / messages |
Social features (friends, DMs) |
user_blocks / reports |
Safety: blocking and content reporting |
referrals |
Referral tracking with qualification logic |
stripe_processed_events |
Webhook idempotency |
All edge functions are Deno-based and located in supabase/functions/. They share common helpers from _shared/ (auth resolution, Stripe client, email utilities, HTTP response formatting).
| Function | Purpose |
|---|---|
complete-registration |
Creates user record in PostgreSQL after Supabase Auth signup |
verify-email |
Email verification flow |
forgot-password |
Initiate password reset (sends email via Resend) |
reset-password |
Complete password reset |
account-actions |
Account management (update profile, delete account, etc.) |
| Function | Purpose |
|---|---|
create-checkout |
Create Stripe checkout session for subscription |
create-portal |
Generate Stripe billing portal link |
stripe-webhook |
Process Stripe events (subscription lifecycle, idempotent) |
hearts |
Hearts system logic (check balance, spend, refill) |
| Function | Purpose |
|---|---|
generate-deck |
AI flashcard generation from text/documents via Gemini |
generate-class |
AI class generation from notes |
ai-limits |
Check and enforce AI generation rate limits |
canvas-lms |
Canvas LMS integration (course/assignment sync) |
| Function | Purpose |
|---|---|
group-actions |
Create, join, manage study groups |
group-sessions |
Real-time cram session management |
accept-shared-deck |
Accept a shared deck into user's library |
| Function | Purpose |
|---|---|
admin-actions |
Admin panel operations (user management, announcements) |
referrals |
Referral code tracking and qualification |
# Deploy a specific function
npx supabase functions deploy <function-name>
# Example
npx supabase functions deploy complete-registration
# Deploy all functions
npx supabase functions deployRequired:
| Variable | Description | How to Get |
|---|---|---|
DATABASE_URL |
Supabase PostgreSQL connection string | Supabase dashboard → Settings → Database |
JWT_SECRET |
Token signing key | openssl rand -base64 32 |
Optional:
| Variable | Description | Default |
|---|---|---|
SUPABASE_URL |
Supabase project URL | — |
SUPABASE_SERVICE_ROLE_KEY |
Service role key for privileged ops | — |
GEMINI_API_KEY |
Google Gemini API key | — |
STRIPE_SECRET_KEY |
Stripe server key | — |
STRIPE_WEBHOOK_SECRET |
Stripe webhook signing secret | — |
RESEND_API_KEY |
Resend email API key | — |
GOOGLE_CLIENT_ID |
Legacy Google ID-token verification for the compatibility bridge | — |
APPLE_CLIENT_ID |
Apple Sign-In service ID | — |
APPLE_TEAM_ID |
Apple team identifier | — |
APPLE_KEY_ID |
Apple private key ID | — |
APPLE_PRIVATE_KEY |
Apple private key (PEM) | — |
RC_WEBHOOK_SECRET |
RevenueCat webhook Bearer token — set via npx supabase secrets set RC_WEBHOOK_SECRET=… (used by the revenuecat-webhook Edge Function) |
— |
| FRONTEND_URL | Frontend URL for redirects | http://localhost:5173 |
| PORT | API server port | 3000 |
| ALLOWED_ORIGINS | CORS origins (comma-separated) | — |
| NODE_ENV | Environment | development |
| Variable | Description | Default |
|---|---|---|
VITE_API_URL |
Backend API URL (blank for local dev) | — |
VITE_STRIPE_PRICE_MONTHLY |
Stripe Price ID for monthly Supporter | — |
VITE_STRIPE_PRICE_ANNUAL |
Stripe Price ID for yearly Supporter | — |
VITE_RC_IOS_API_KEY |
RevenueCat iOS Public API Key (appl_xxxxx) — native IAP only |
— |
Web/PWA Google Sign-In uses the Supabase-hosted redirect flow, so there is no VITE_GOOGLE_CLIENT_ID client variable in this setup. Configure the Supabase callback URI in Google Cloud and the /account return URLs in Supabase Auth as documented in the setup section above.
Edge function secrets are set via the Supabase dashboard or CLI:
npx supabase secrets set STRIPE_SECRET_KEY=sk_...
npx supabase secrets set RESEND_API_KEY=re_...
npx supabase secrets set GEMINI_API_KEY=...
# Stripe recurring price IDs (must match client VITE_* values). Optional STRIPE_PRICE_MONTHLY overrides default test/live id in code.
npx supabase secrets set STRIPE_PRICE_ANNUAL=price_...
# Canonical https origin for Stripe success/cancel URLs when the client Origin is Capacitor (required for iOS checkout)
npx supabase secrets set CLIENT_URL=https://your-production-domain| Command | Description |
|---|---|
npm start |
Run client + server concurrently (dev mode) |
npm run server |
Run server only (nodemon) |
npm run client |
Run client only (Vite) |
| Command | Description |
|---|---|
npm start |
Start server (node index.js) |
npm run dev |
Start server with auto-reload (nodemon) |
npm test |
Run backend tests (vitest) |
| Command | Description |
|---|---|
npm run dev |
Start Vite dev server |
npm run build |
Production build |
npm run build:ios |
vite build + cap sync ios |
npm run ios:sync |
Sync web assets to the Xcode project |
npm run ios:open |
Open the iOS project in Xcode |
npm run preview |
Preview production build locally |
npm run lint |
Run ESLint |
npm test |
Run frontend tests (vitest) |
Both client and server use Vitest.
# Server tests (Vitest + Supertest for HTTP assertions)
cd server && npm test
# Client tests (Vitest + Testing Library + jsdom)
cd client && npm testserver/test/ # Backend integration tests
client/src/test/ # Frontend component tests
Riven is deployed across three services:
The project is configured as a Vercel monorepo via vercel.json:
/api/*→server/index.js(deployed as a serverless function via@vercel/node)/*→client/(built via@vercel/static-build, serves fromdist/)
Steps:
- Push to GitHub
- Import project in Vercel dashboard
- Set Root Directory to
./ - Add all environment variables from
server/.envandclient/.envto Vercel project settings - Deploy — Vercel auto-builds on each push
- Database: PostgreSQL hosted on Supabase with connection pooling
- Edge Functions: Deno-based serverless functions deployed via Supabase CLI
- Auth: Supabase Auth handles signup/login flows
# Deploy a single edge function
npx supabase functions deploy <function-name>
# Run migrations
npx supabase db pushThe client ships with Capacitor 8 and an Xcode project under client/ios/. Native Google Sign-In, native iOS Sign in with Apple, auth storage, and signup (without Turnstile in the WebView) are already wired in the React app.
Prerequisites: macOS, Xcode, CocoaPods (if prompted by Capacitor), Node 20+.
Build and open in Xcode
cd client
cp .env.example .env # then set VITE_* (see below)
npm run build:ios # vite build + cap sync ios
npm run ios:open # optional: open App.xcworkspace / project in XcodeOr step-by-step: npm run build, npm run ios:sync, then open client/ios/App/App.xcodeproj from Xcode.
Environment (baked in at vite build time)
| Variable | Notes |
|---|---|
VITE_API_URL |
Simulator: default http://localhost:3000/api can reach the API on your Mac. Physical device: must be your Mac’s LAN IP or a deployed HTTPS API (e.g. https://api.example.com/api). |
VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY |
Required. |
VITE_GOOGLE_WEB_CLIENT_ID |
Web client ID (same as Supabase Auth → Google). Required for native Google Sign-In. |
VITE_STRIPE_PRICE_* |
If you test checkout from the app. |
Google Sign-In (iOS) — Create an iOS OAuth client in Google Cloud for bundle ID com.riven.app. Copy values into client/ios/debug.xcconfig as GOOGLE_IOS_CLIENT_ID and GOOGLE_IOS_URL_SCHEME (reversed client ID). Use a non-committed Release xcconfig or Xcode build settings for production signing.
Sign in with Apple (iOS native) — The Capacitor app uses native Apple auth on iOS only, then exchanges the Apple ID token with Supabase via signInWithIdToken(...).
- In the Apple Developer portal, enable Sign in with Apple for the App ID / bundle ID
com.riven.app. - In the Supabase dashboard, enable the Apple auth provider for the hosted project. For hosted Supabase, this dashboard configuration is the source of truth; the repo
supabase/config.tomlApple block is only a local/self-host reference. - In the Express API, set
APPLE_CLIENT_IDinserver/.envso the legacy/api/auth/oauth/applebridge can still verify Apple tokens for users who fall back to legacy 2FA. - Open the iOS target in Xcode and confirm the Sign in with Apple capability is present under Signing & Capabilities.
Production API CORS — Ensure ALLOWED_ORIGINS on the Express server includes capacitor://localhost (defaults already do). Add ionic://localhost if you see that Origin in logs.
Stripe checkout from the app — Edge Functions use the CLIENT_URL secret when the request Origin is not a usable HTTPS URL (e.g. Capacitor). Set Supabase secret CLIENT_URL to your canonical web app origin (e.g. https://riven.rocks) so post-payment redirects land in the browser; users return to the native app separately.
Routing — The native shell uses HashRouter so deep links and reloads work with bundled static files. Web/PWA builds keep BrowserRouter.
App Store preparation — See docs/ios-app-store.md (Sign in with Apple, IAP vs Stripe, ATS, ads).
Error: connect ECONNREFUSED 127.0.0.1:5432
Fix: Check DATABASE_URL format. For Supabase, use the pooler connection string from Settings → Database → Connection string (URI).
Error: JsonWebTokenError: invalid signature
Fix: JWT_SECRET must stay consistent. Users need to log out and back in if the secret changes.
Error: Network errors or CORS issues in dev
Fix: Ensure the server is running on port 3000. Vite proxies /api requests automatically in dev. Check ALLOWED_ORIGINS includes your frontend URL.
The server auto-migrates columns on every startup. If you see column-not-found errors, restart the server — it runs ALTER TABLE ADD COLUMN IF NOT EXISTS migrations in db.js.
Fix: Ensure STRIPE_WEBHOOK_SECRET matches your Stripe dashboard. For local dev:
stripe listen --forward-to localhost:3000/api/webhooks/stripeFix: Check edge function logs:
npx supabase functions logs <function-name>Ensure all required secrets are set:
npx supabase secrets listError: Command not found: vite
Fix: Run npm install inside client/.