AI-powered study schedule optimizer for students. MoreTime helps you manage coursework, extract tasks from syllabi, chat with an AI assistant that understands your workload, and generate optimized day-by-day study plans — all from a native iOS app backed by a Node.js API.
- Features
- Architecture
- Tech Stack
- Project Structure
- Getting Started
- Environment Variables
- API Reference
- Database Schema
- Tests
- Deployment
- Create, edit, and delete tasks with due dates, priority levels (1–5), estimated hours, and course assignments
- Group tasks by course with color-coded indicators
- Sort by due date, priority, or creation date
- Swipe-to-complete and swipe-to-delete gestures
- Mark tasks as pending, in-progress, or completed
- Optional learning debrief after you mark a task complete (confidence, hardest part, optional “revisit” note) — saved to your profile for Chat context; see Learning reflections below
- Short post-completion reflection (from Tasks swipe-to-complete or Task detail when moving to completed); Skip saves nothing; Save appends to profile
preferences.learningDebriefs(JSON array, last 25 entries) - Settings → Past reflections: browse saved debriefs, pull to refresh from the server, or Clear all to remove them from your profile
- The chat backend injects recent reflections into the assistant system prompt so replies can personalize study advice (no separate ML model or agent)
- Context-aware chat that knows your tasks, schedule, and courses
- Uses recent learning reflections from your profile when present (see Learning reflections above)
- Paperclip attachments: upload assignment PDFs/DOCX/images from chat; after parsing, the assistant uses the document text to schedule tasks (same upload + parse pipeline as Files)
- Automatically detects and creates tasks from natural conversation (e.g., "I have a CS310 paper due Friday")
- Auto-generates schedule blocks whenever a task is created via chat
- Persistent conversation history with session management
- Suggestion chips for quick prompts
- Upload PDF, DOCX, TXT, or image files (syllabi, assignment sheets)
- AI-powered document classification (syllabus vs. single assignment)
- Syllabi: extracts all assignments, exams, and deadlines with estimated hours
- Assignments: breaks down a single deliverable into actionable subtasks spread across available days
- OCR fallback for scanned PDFs and images via GPT-4o vision
- AI analyzes all pending tasks, deadlines, priorities, and locked blocks to produce an optimal study plan
- Respects student preferences (study hours, max hours/day, break duration)
- Never schedules over locked blocks (classes, work, etc.)
- Validates generated blocks for time conflicts and format correctness with up to 3 retry attempts
- Auto-escalates priority of overdue tasks
- Each block includes a specific label describing what to work on during that session
- Record audio and send to AI chat via voice
- Azure OpenAI transcription (gpt-4o-transcribe-diarize)
- Real-time audio level visualization with waveform display
- Monthly calendar grid with color-coded indicators: circles for schedule blocks, small squares for tasks that have a due date but no matching block (deduped when a block already links the same task)
- Day detail separates Scheduled (time blocks from
/schedule: classes and planned study sessions) from Due (tasks from your Tasks tab due that day without a matching block). If your task list is empty, you may still see Scheduled rows from classes or generated schedule — those are not task-list items. Tap a task under Due to open its detail - Toolbar Clear menu: Clear all schedule (removes every block, including locked class times) and Clear current day (removes all blocks on the selected date and deletes pending/in-progress tasks due that local day)
- Clear Schedule (toolbar): deletes non-locked blocks on the server, refetches the calendar, then updates the UI. Locked class blocks stay. Tasks with due dates can still appear under Due until you edit or remove those tasks
- Navigate between months, jump to today
- Locked blocks (recurring classes) shown with lock icon
- Semester tab: upload multiple syllabi (PDF/DOCX), map files to course names, pick semester dates, generate an AI week-by-week workload view (intensity, crunch weeks, events list)
- Apply to Calendar creates tasks from plan events via
POST /tasks - One plan per user: the generated
SemesterPlanis stored in profilepreferences.semesterPlan(JSON string). New Plan clears local state and removes that preference viaPATCH /auth/me - Reopening the Semester tab restores the saved plan after
GET /auth/me(if present)
- Settings → Courses & class schedule (sheet): manage courses, add recurring locked class blocks to the calendar (with Repeat until end date for weekly repetition)
- Delete course: from the course edit sheet (Delete course), swipe-to-delete on the list, or clear the class picker when that course is removed
- Delete scheduled class: open the class in the editor (Delete from schedule) or swipe left on a row in Scheduled classes (locked)
- Create courses with custom names and hex colors
- Tasks and schedule blocks can be associated with courses (optional for some tasks)
- Task count displayed per course in the course list
┌──────────────────┐ ┌──────────────────────┐
│ iOS App │ HTTP │ Express API │
│ (SwiftUI) │ ◄─────► │ (Node.js + TS) │
│ │ │ │
│ @Observable │ │ Routes → Services │
│ Stores ──► API │ │ ↓ │
│ Client │ │ ┌──────────┐ │
│ │ │ │ Supabase │ Auth + DB │
│ Keychain tokens │ │ └──────────┘ │
│ │ │ ┌──────────────────┐ │
│ │ │ │ Azure OpenAI │ │
│ │ │ │ Chat, Vision, │ │
│ │ │ │ Scheduling, │ │
│ │ │ │ Transcription │ │
│ │ │ └──────────────────┘ │
└──────────────────┘ └────────────────────────┘
- Express routes handle HTTP requests with Zod validation middleware
- Supabase for authentication (Admin API for user creation, session tokens for auth) and Postgres database
- Azure OpenAI for all AI features: chat completions, document parsing, schedule generation, image OCR, and audio transcription
- Service layer separates business logic (chat context building, schedule optimization, file parsing, learning-debrief formatting for chat) from route handlers
- snake_case ↔ camelCase transform layer between Supabase (snake) and API responses (camel)
- Rate limiting: 200 req/15 min global; stricter limiter (20 req/min) on
/chat,/files, and/voice(AI-heavy routes)
- @Observable stores (
AuthStore,TaskStore,ScheduleStore,ChatStore,SemesterStore) manage state and are injected via SwiftUI's.environment() - APIClient singleton handles all networking with automatic token refresh on 401 responses
- KeychainHelper stores auth tokens securely
- ErrorLogger captures and surfaces API errors via toast banners and a debug log
- Profile preferences (
PATCH /auth/me): merged client-side; used for study-time prefs, persisted semester plan (semesterPlankey), and learning debriefs (learningDebriefsarray) - Tab bar: Calendar, Tasks, Chat, Semester, Settings — switching to Calendar refreshes tasks and loaded schedule range
- Targets iOS 17+ using
@Observable(not Combine); tab bar uses iOS 17–compatible.tabItem/.tagAPIs
| Layer | Technology |
|---|---|
| iOS App | Swift, SwiftUI, SwiftData, iOS 17+ |
| Backend | Node.js 24+, Express, TypeScript |
| Database | Supabase (PostgreSQL) |
| Auth | Supabase Auth (Admin API + session tokens) |
| AI | Azure OpenAI (GPT-4o for chat/vision/scheduling) |
| Speech | Azure OpenAI audio API (gpt-4o-transcribe-diarize; same endpoint/key as chat) |
| File Parsing | pdf-parse, mammoth (DOCX), GPT-4o vision (OCR) |
| Validation | Zod |
| Testing | Vitest (backend/tests) |
| Deployment | Azure Web App via GitHub Actions |
MoreTime/
├── backend/
│ ├── src/
│ │ ├── index.ts # Express app entry point
│ │ ├── routes/
│ │ │ ├── auth.ts # Register, login, refresh, logout, profile
│ │ │ ├── courses.ts # CRUD for courses
│ │ │ ├── tasks.ts # CRUD for tasks
│ │ │ ├── schedule.ts # Schedule blocks + AI generation
│ │ │ ├── chat.ts # AI chat messages
│ │ │ ├── files.ts # File upload, task extraction, semester-plan API
│ │ │ └── voice.ts # Audio transcription + voice chat
│ │ ├── services/
│ │ │ ├── ai.ts # Azure OpenAI (chat, extract, schedule, semester plan)
│ │ │ ├── chat.ts # Chat context builder + action parser
│ │ │ ├── scheduling.ts # Schedule generation, validation, semester week grouping
│ │ │ ├── fileParser.ts # PDF, DOCX, TXT, image parsing
│ │ │ └── voice.ts # Audio transcription via Azure
│ │ ├── middleware/
│ │ │ ├── auth.ts # Bearer token auth guard (Supabase)
│ │ │ ├── validate.ts # Zod request validation
│ │ │ └── errorHandler.ts # Global error handler
│ │ └── utils/
│ │ ├── supabase.ts # Supabase client singleton
│ │ ├── azure-openai.ts # OpenAI client singleton
│ │ ├── env.ts # Environment variable validation
│ │ ├── errors.ts # Custom error classes
│ │ ├── transform.ts # snake_case ↔ camelCase
│ │ └── learningDebriefs.ts # Format stored reflections for chat system prompt
│ ├── tests/ # Vitest (validation, scheduling)
│ ├── supabase-migration.sql # Database schema + RLS policies
│ ├── package.json
│ └── tsconfig.json
│
├── ios/
│ └── MoreTime/
│ ├── MoreTimeApp.swift # App entry point
│ ├── Views/
│ │ ├── RootView.swift # Auth routing (login vs main)
│ │ ├── LoginView.swift # Sign in + registration
│ │ ├── MainTabView.swift # Tab bar (Calendar, Tasks, Chat, Semester, Settings)
│ │ ├── CalendarView.swift # Calendar + merged due tasks + day detail
│ │ ├── SemesterHeatMapView.swift # Semester heat map + apply to calendar
│ │ ├── TaskListView.swift # Task list with grouping + sorting
│ │ ├── TaskDetailView.swift # Task edit/create form
│ │ ├── ChatView.swift # AI chat interface
│ │ ├── VoiceInputView.swift # Voice recording UI
│ │ ├── SettingsView.swift # Settings, study prefs, courses & class schedule sheet
│ │ ├── LearningDebriefSheet.swift # Post-completion reflection form
│ │ ├── PastReflectionsView.swift # Settings: list of saved debriefs
│ │ ├── ScheduleGenerateView.swift # Schedule generation UI
│ │ ├── FileUploadView.swift # File upload + task extraction
│ │ └── CourseManagementView.swift # Course CRUD
│ ├── Stores/
│ │ ├── AuthStore.swift # Auth state management
│ │ ├── TaskStore.swift # Tasks + courses state
│ │ ├── ScheduleStore.swift # Schedule blocks state
│ │ ├── ChatStore.swift # Chat messages state
│ │ └── SemesterStore.swift # Semester plan, file upload helpers, apply-to-tasks
│ ├── Services/
│ │ ├── APIClient.swift # HTTP client with token refresh
│ │ ├── KeychainHelper.swift # Secure token storage
│ │ ├── AudioRecorder.swift # AVAudioRecorder wrapper
│ │ └── ErrorLogger.swift # Error capture + toast banner
│ ├── Models/
│ │ ├── APIModels.swift # Codable DTOs for all endpoints
│ │ └── CachedModels.swift # SwiftData models (local cache)
│ └── Components/
│ ├── ColorExtension.swift # Color(hex:) initializer
│ └── ErrorBanner.swift # Global error overlay modifier
│
├── .github/workflows/
│ └── main_moretime.yml # CI/CD pipeline
└── .gitignore
- Node.js 24+ and npm
- Xcode 15+ with iOS 17+ SDK
- A Supabase project (supabase.com)
- An Azure OpenAI resource with a GPT-4o deployment
-
Create a new project at supabase.com/dashboard
-
Open the SQL Editor and run the migration file to create all tables, indexes, triggers, and RLS policies:
-- Copy and paste the contents of backend/supabase-migration.sqlThis creates: profiles, courses, tasks, schedule_blocks, file_uploads, chat_messages
It also creates a trigger (on_auth_user_created) that automatically inserts a profiles row whenever a new user signs up via Supabase Auth.
- Go to Project Settings → API and copy:
- Project URL →
SUPABASE_URL - service_role secret key →
SUPABASE_SERVICE_ROLE_KEY(used by the Node API only; keep server-side)
- Project URL →
The backend does not require the Supabase anon key. Use the anon key only if you add a Supabase client directly in a mobile or web app.
cd backend
npm install # runs `tsc` via postinstall to emit `dist/`
npm run dev # starts dev server with hot reload (default port 3000)Set the variables from Environment Variables in your shell or IDE before starting the server. The backend reads process.env only; it does not load a .env file unless you add a loader (for example dotenv) or configure your editor to inject env vars.
The dev server runs at http://localhost:3000. Test with:
curl http://localhost:3000/health- Open
ios/MoreTime.xcodeprojin Xcode - Set the API base URL in
ios/MoreTime/Services/APIClient.swift(baseURL) — e.g. your deployed Azure Web App orhttp://localhost:3000for a local backend - Build and run on the iOS Simulator (or a physical device)
For development without authentication, set DEV_BYPASS_AUTH=true in the environment that runs the API (same as your other backend variables). The iOS app has a corresponding #if DEBUG block in AuthStore.swift that auto-authenticates with a dev user. Remove or disable this block when testing real auth flows.
Required variables are checked in backend/src/utils/env.ts the first time the API needs Supabase or Azure OpenAI (lazy init). DEV_BYPASS_AUTH and NODE_ENV are read directly from process.env. Supply variables via your shell, IDE run configuration, or hosting provider (e.g. Azure App Settings). They are not loaded from a .env file unless you add that yourself.
| Variable | Required | Description |
|---|---|---|
SUPABASE_URL |
Yes | Your Supabase project URL (e.g., https://xxx.supabase.co) |
SUPABASE_SERVICE_ROLE_KEY |
Yes | Supabase service role key (secret — server-side only) |
AZURE_OPENAI_ENDPOINT |
Yes | Azure OpenAI resource endpoint (base URL) |
AZURE_OPENAI_API_KEY |
Yes | Azure OpenAI API key (chat, vision, scheduling, transcription) |
AZURE_OPENAI_DEPLOYMENT_NAME |
No | Chat / vision deployment name (default: gpt-4o) |
PORT |
No | Server port (default: 3000) |
DEV_BYPASS_AUTH |
No | Set to true to skip auth in development (Dev Bypass) |
NODE_ENV |
No | Set to production to hide error details in API responses |
All endpoints (except auth and health) require a Bearer <token> header. Tokens are Supabase session access tokens obtained from login/register.
| Method | Path | Body | Response |
|---|---|---|---|
POST |
/register |
{ email, name, password, timezone? } |
{ user, accessToken, refreshToken } |
POST |
/login |
{ email, password } |
{ user, accessToken, refreshToken } |
POST |
/refresh |
{ refreshToken } |
{ accessToken, refreshToken } |
POST |
/logout |
— | { message } |
GET |
/me |
— | UserProfile |
PATCH |
/me |
{ name?, timezone?, preferences? } |
UserProfile (full preferences JSON is replaced with the merged object from the client; optional semesterPlan string for the semester heat map; optional learningDebriefs array of reflection objects) |
| Method | Path | Body | Response |
|---|---|---|---|
GET |
/ |
— | [Course] (includes task count) |
GET |
/:id |
— | Course (includes tasks) |
POST |
/ |
{ name, color?, metadata? } |
Course |
PATCH |
/:id |
{ name?, color?, metadata? } |
Course |
DELETE |
/:id |
— | 204 |
| Method | Path | Body / Query | Response |
|---|---|---|---|
GET |
/ |
Query: courseId?, status?, sortBy?, sortOrder? |
[TaskItem] |
GET |
/:id |
— | TaskItem (includes course + schedule blocks) |
POST |
/ |
{ courseId?, title, description?, dueDate?, priority?, estimatedHours?, status? } |
TaskItem |
PATCH |
/:id |
Same fields, all optional | TaskItem |
DELETE |
/:id |
— | 204 |
DELETE |
/clear |
— | { removed } |
DELETE |
/due-in-day |
Query: start, end (ISO-8601 instants, half-open [start,end)) |
{ removed } — pending / in-progress tasks with due_date in range |
| Method | Path | Body / Query | Response |
|---|---|---|---|
GET |
/ |
Query: startDate, endDate (YYYY-MM-DD) |
[ScheduleBlock] |
POST |
/ |
{ taskId?, date, startTime, endTime, isLocked?, label? } |
ScheduleBlock |
PATCH |
/:id |
Same fields, all optional | ScheduleBlock |
DELETE |
/:id |
— | 204 |
DELETE |
/clear |
— | { removed } — non-locked blocks only (is_locked = false) |
DELETE |
/clear-all |
— | { removed } — all blocks for the user (locked + generated) |
DELETE |
/day |
Query: date (YYYY-MM-DD) |
{ removed } — all blocks on that calendar date |
POST |
/generate |
— | { blocksCreated, blocksRemoved, blocks, warnings } |
| Method | Path | Body | Response |
|---|---|---|---|
POST |
/message |
{ message?, sessionId?, fileIds? } — provide a non-empty message and/or at least one fileId (max 5) |
{ sessionId, response, action?, scheduleGenerated? } |
Attachments: Upload files with POST /files/upload, poll GET /files/:id until parseStatus is completed, then send their IDs in fileIds. The server injects parsed text into the model for that turn only (not stored in full in chat history). Chat messages store a short [Attachments: …] line instead.
Context: The assistant system prompt includes your pending tasks, today’s blocks, courses, and recent learning reflections from profiles.preferences.learningDebriefs when present (formatted in backend/src/utils/learningDebriefs.ts).
The AI may return an action of type task_created with the created task data. When this happens, the schedule is automatically regenerated in the background.
| Method | Path | Body | Response |
|---|---|---|---|
POST |
/upload |
Multipart: files + optional courseId |
[FileUploadResponse] |
GET |
/ |
— | [FileUploadResponse] |
GET |
/:id |
— | FileUploadResponse |
DELETE |
/:id |
— | 204 |
POST |
/semester-plan |
{ fileIds: string[], semesterStart, semesterEnd } (dates YYYY-MM-DD) |
{ weeks, crunchWeeks, totalEvents, semesterStart, semesterEnd } |
POST |
/:id/extract-tasks |
{ dueDate? } |
{ extractedCount, tasks, documentType } |
Uploaded files are parsed asynchronously. Poll GET /:id until parseStatus is completed. The extract endpoint auto-detects whether the document is a syllabus (extracts all assignments) or a single assignment (breaks it into subtasks).
| Method | Path | Body | Response |
|---|---|---|---|
POST |
/transcribe |
Multipart: audio |
{ text } |
POST |
/chat |
Multipart: audio + optional sessionId |
{ transcription, sessionId, response } |
| Method | Path | Response |
|---|---|---|
GET |
/health |
{ status: "ok", timestamp } |
All tables use UUIDs as primary keys. Row Level Security (RLS) is enabled on every table — users can only access their own data.
| Table | Key Columns | Notes |
|---|---|---|
profiles |
id (FK → auth.users), email, name, timezone, preferences (JSONB) |
Auto-created via trigger on auth signup; may include semesterPlan (string) for the saved heat map and learningDebriefs (array) for post-task reflections used in chat |
courses |
id, user_id, name, color |
|
tasks |
id, user_id, course_id, title, due_date, priority, estimated_hours, status |
course_id set null on course delete |
schedule_blocks |
id, user_id, task_id, course_id, date, start_time, end_time, is_locked, label |
Optional course_id → courses for class blocks; API embeds classCourse when FK schedule_blocks_course_id_fkey exists in Supabase |
file_uploads |
id, user_id, course_id, original_name, storage_path, mime_type, file_size, parsed_content, parse_status, parsed_at |
Upload metadata + async parse pipeline; status: pending → parsing → completed/failed |
chat_messages |
id, user_id, role, content, session_id, timestamp |
Roles: user, assistant |
See backend/supabase-migration.sql for the complete schema, indexes, trigger function, and RLS policies.
From the backend directory:
npm test # run once (Vitest)
npm run test:watchThe backend deploys to Azure Web App via GitHub Actions when main changes under backend/** or when the workflow file .github/workflows/main_moretime.yml changes. You can also run the workflow manually (Actions → workflow_dispatch).
- Checkout code
- Set up Node.js 24.x
npm install→npm run build→npm prune --omit=dev- Remove
.envfiles (secrets are configured in Azure App Settings) - Upload build artifact
- Deploy to Azure Web App
moretime(Production slot) using OIDC auth
- API:
https://moretime-gdbwhjgfdxeyhtfw.canadacentral-01.azurewebsites.net - Health check:
GET /health
Environment variables in production are set via Azure App Settings, not .env files.