diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..3b0b40372
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env
\ No newline at end of file
diff --git a/AUTH_SIMULATION.md b/AUTH_SIMULATION.md
new file mode 100644
index 000000000..5adcd8701
--- /dev/null
+++ b/AUTH_SIMULATION.md
@@ -0,0 +1,126 @@
+# Authentication Simulation Guide
+
+## Current Setup: Mock Authentication
+
+Since your teammate is working on real authentication, we've set up a **mock auth system** that simulates being logged in as "George Demo".
+
+## How It Works
+
+### 1. Mock Auth Context (`client/src/context/AuthContext.jsx`)
+- Simulates a logged-in user
+- Provides user data throughout the app
+- **Default user**: `george-demo-id` (change this to match your DB)
+
+### 2. Getting User Info Anywhere
+
+In any component:
+
+```jsx
+import { useAuth } from "../context/AuthContext";
+
+function MyComponent() {
+ const { user, isAuthenticated } = useAuth();
+
+ // user.id → "george-demo-id"
+ // user.username → "George Demo"
+ // user.email → "george@example.com"
+
+ // Use user.id when creating posts, joining groups, etc.
+ const createSomething = async () => {
+ await api.post("/something", {
+ user_id: user.id, // Uses George's ID
+ // ... other data
+ });
+ };
+}
+```
+
+### 3. Current Implementations
+
+**Posts** (`Home.jsx`):
+- ✅ Uses `user.id` when creating posts
+- Posts are attributed to "George Demo"
+
+## How to Change the Mock User
+
+Open `client/src/context/AuthContext.jsx` and update:
+
+```jsx
+const [user, setUser] = useState({
+ id: "YOUR_ACTUAL_USER_ID_FROM_DB", // ← Change this
+ username: "George Demo",
+ email: "george@example.com",
+});
+```
+
+**To find George's actual ID from Supabase:**
+1. Go to Supabase Dashboard
+2. Open the `user` table
+3. Find George Demo's row
+4. Copy the `id` value
+5. Paste it in the code above
+
+## When Real Auth is Ready
+
+Your teammate will replace the mock system with real authentication:
+
+1. **Login/Signup** will call real API endpoints
+2. **JWT token** will be stored in localStorage
+3. **Token** will be sent with every API request
+4. **Backend** will validate token and extract real user ID
+
+## What You Can Do Now
+
+You can implement features that require a logged-in user:
+
+- **Create posts** → Uses George's ID
+- **Join groups** → Add George to group members
+- **Send messages** → Attribute to George
+- **Like/comment** → Track George's interactions
+
+Just use `user.id` from the `useAuth()` hook everywhere!
+
+## Backend Side (Optional Enhancement)
+
+If you want the backend to validate the user, you could add middleware later:
+
+```javascript
+// server/middleware/auth.js (for future)
+export const authMiddleware = (req, res, next) => {
+ // For now, trust the user_id in the request
+ // Later: validate JWT token here
+ next();
+};
+```
+
+## Testing Different Users
+
+To test as a different user, change the ID in `AuthContext.jsx`:
+
+```jsx
+// Test as User 1
+const [user, setUser] = useState({
+ id: "user-1-id",
+ username: "User One",
+ // ...
+});
+
+// Test as User 2
+const [user, setUser] = useState({
+ id: "user-2-id",
+ username: "User Two",
+ // ...
+});
+```
+
+## Transition Plan
+
+When real auth is implemented:
+
+1. Keep `AuthContext.jsx` structure the same
+2. Replace mock `login()` with real API call
+3. Add token storage/retrieval
+4. Update `user` state from API response
+5. Add token to API requests (already set up in `api.js`)
+
+The rest of your code won't need to change! 🎉
diff --git a/INTEGRATION_README.md b/INTEGRATION_README.md
new file mode 100644
index 000000000..337f29725
--- /dev/null
+++ b/INTEGRATION_README.md
@@ -0,0 +1,137 @@
+# Frontend-Backend Integration - Posts Feature
+
+## ✅ Completed Integration
+
+The frontend and backend are now connected for **Post CRUD operations**. All users (even unauthenticated) can create, read, update, and delete posts.
+
+## 🏗️ What Was Implemented
+
+### Backend (Already Complete)
+- ✅ Express server with CORS enabled
+- ✅ Supabase database integration
+- ✅ Full CRUD API endpoints at `/api/posts`
+ - `GET /api/posts` - Get all posts
+ - `GET /api/posts/:id` - Get single post
+ - `POST /api/posts` - Create new post
+ - `PUT /api/posts/:id` - Update post
+ - `DELETE /api/posts/:id` - Delete post
+
+### Frontend (Newly Added)
+- ✅ Axios installed and configured
+- ✅ API service layer created
+- ✅ Vite proxy configured
+- ✅ Post UI components integrated into Home page
+- ✅ Full CRUD operations in UI
+
+## 📁 New Files Created
+
+```
+client/
+├── .env # Environment variables
+├── src/
+│ ├── services/
+│ │ ├── api.js # Axios base configuration
+│ │ └── postService.js # Post API calls
+│ └── pages/
+│ └── Posts.css # Post styling
+```
+
+## 🚀 How to Run
+
+### 1. Start the Backend Server
+```bash
+cd server
+npm install
+npm run dev
+```
+Server runs on: `http://localhost:3000`
+
+### 2. Start the Frontend
+```bash
+cd client
+npm install
+npm run dev
+```
+Client runs on: `http://localhost:5173`
+
+## 🎯 Features Implemented
+
+### Create Post
+- Fill in caption, content, and optional photo URL
+- Click "Post" button
+- Post is created in Supabase database
+
+### Read Posts
+- All posts automatically load when Home page opens
+- Posts display with user, timestamp, caption, content, and image
+
+### Update Post
+- Click "Edit" button on any post
+- Modify caption, content, or photo URL
+- Click "Update" to save changes
+
+### Delete Post
+- Click "Delete" button on any post
+- Confirm deletion in popup
+- Post is removed from database
+
+## 🔧 Post Data Structure
+
+Based on `server/tables/post.json`:
+```json
+{
+ "id": "auto-generated",
+ "user_id": "anonymous (for now)",
+ "photo_url": "optional image URL",
+ "caption": "post title/caption",
+ "content": "post body text",
+ "user_ids": {},
+ "group_limit": ""
+}
+```
+
+## 🌐 API Endpoints Used
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | `/api/posts` | Fetch all posts |
+| GET | `/api/posts/:id` | Fetch single post |
+| POST | `/api/posts` | Create new post |
+| PUT | `/api/posts/:id` | Update existing post |
+| DELETE | `/api/posts/:id` | Delete post |
+
+## 🎨 UI Location
+
+Posts are displayed on the **Home page** (`/home`) below the groups section.
+
+## ⚠️ Important Notes
+
+1. **Authentication is disabled** - As requested, anyone can create/edit/delete posts
+2. **Supabase must be configured** - Make sure `server/.env` has valid Supabase credentials
+3. **Proxy is configured** - Frontend `/api` calls are proxied to `http://localhost:3000`
+4. **Both servers must run** - Backend on port 3000, frontend on port 5173
+
+## 🔮 Next Steps (When Ready)
+
+- Add authentication (your teammate's work)
+- Restrict edit/delete to post owners
+- Add user profiles
+- Implement groups integration
+- Add real-time updates
+- Image upload functionality
+
+## 🐛 Troubleshooting
+
+### "Failed to load posts"
+- Ensure backend server is running on port 3000
+- Check Supabase credentials in `server/.env`
+- Verify `post` table exists in Supabase
+
+### "Network Error"
+- Backend server not running
+- Check console for CORS errors
+
+### Posts not showing after creation
+- Check browser console for errors
+- Verify Supabase table has correct schema
+- Check network tab to see API responses
diff --git a/README.md b/README.md
index 0e1211217..2dab3be51 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,71 @@
-# [your app name here]
+# The Lexington Link
CodePath WEB103 Final Project
-Designed and developed by: [your names here]
+Designed and developed by: Evan Lu, James Chen, Eric Azayev
-🔗 Link to deployed app:
+🔗 Link to deployed app:
-## About
+## About / Description and Purpose
-### Description and Purpose
+The Lexington Link is a full-stack web application designed exclusively for Hunter College students to combat social isolation and foster a more connected campus community.
+
+The app serves as a focused friend and hangout matching platform, moving beyond superficial connections by matching users based on shared academic courses, compatible majors, and preferred local hangout spots.
+
+The primary purpose is to provide students with the tools to easily:
+* Discover and connect with peers who share interests or classes.
+* Form and manage casual hangout groups for socializing, studying, or exploring the city.
+* Schedule and communicate plans within a safe, university-verified environment.
+
+By implementing essential features like school email verification, a familiar swipe interface, and robust Group Formation and Chat Functionality, The Lexington Link aims to directly address the high rates of student loneliness by making healthy, local social connection simple and intuitive.
-[text goes here]
### Inspiration
-[text goes here]
+* Majority of Hunter College students are anti-social, so this build to bridge connections and create a more healthy environment for college lifestyle.
+* Dating Apps: swipe feature allows an easy experience for users.
+* "More broadly, loneliness is a significant issue among college students in the U.S., with a survey indicating nearly two-thirds (64.7%) of college students report feeling lonely. This data highlights the mental health impact of loneliness, including psychological distress." - ActiveMinds (Nonprofit organization)
## Tech Stack
-Frontend:
+Frontend: ReactJS, Tailwind CSS
-Backend:
+Backend: Express, Postgres
## Features
-### [Name of Feature 1]
+### User registration
+
+To connect with other students, users need to create an account with their email.
+
+### Profile creation
+
+✅Users can add personal details, their major, and favorite hangout spots to find compatible hangout buddies.
+
+
+
+### Swipe Functionality
+
+Users can swipe for hangout buddies based on their courses, interests, location, or preferred hangout time.
+
+### Group formation
-[short description goes here]
+Users can form hangout groups, invite friends, and schedule hangout plans.
-[gif goes here]
+### Chat functionality
-### [Name of Feature 2]
+✅Users can chat with their hangout buddies and discuss their hangout plan. user has a list of GCIDs. When they open messages, they're shown all the gcs they're in.
+
-[short description goes here]
-[gif goes here]
+### Home Functionality
-### [Name of Feature 3]
+✅Home can be used to view Clubs, posts.
+✅Brew Random Group in /home
+✅User can access messages from /home
+
-[short description goes here]
-[gif goes here]
### [ADDITIONAL FEATURES GO HERE - ADD ALL FEATURES HERE IN THE FORMAT ABOVE; you will check these off and add gifs as you complete them]
diff --git a/client/.gitignore b/client/.gitignore
new file mode 100644
index 000000000..3b0b40372
--- /dev/null
+++ b/client/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env
\ No newline at end of file
diff --git a/client/README.md b/client/README.md
new file mode 100644
index 000000000..18bc70ebe
--- /dev/null
+++ b/client/README.md
@@ -0,0 +1,16 @@
+# React + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
diff --git a/client/eslint.config.js b/client/eslint.config.js
new file mode 100644
index 000000000..cee1e2c78
--- /dev/null
+++ b/client/eslint.config.js
@@ -0,0 +1,29 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{js,jsx}'],
+ extends: [
+ js.configs.recommended,
+ reactHooks.configs['recommended-latest'],
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module',
+ },
+ },
+ rules: {
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ },
+ },
+])
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 000000000..f07b3d04d
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,13 @@
+
+
+
+ );
+}
+
+export default UserLookup;
diff --git a/client/src/services/api.js b/client/src/services/api.js
new file mode 100644
index 000000000..b0353110a
--- /dev/null
+++ b/client/src/services/api.js
@@ -0,0 +1,48 @@
+import axios from "axios";
+
+// Create axios instance with base configuration
+const api = axios.create({
+ baseURL: import.meta.env.VITE_API_URL || "http://localhost:3000/api",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ timeout: 10000, // 10 second timeout
+});
+
+// Request interceptor (for adding auth tokens later)
+api.interceptors.request.use(
+ (config) => {
+ // You can add auth token here when authentication is ready
+ // const token = localStorage.getItem('token');
+ // if (token) {
+ // config.headers.Authorization = `Bearer ${token}`;
+ // }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+// Response interceptor (for handling errors globally)
+api.interceptors.response.use(
+ (response) => {
+ return response;
+ },
+ (error) => {
+ // Handle errors globally
+ if (error.response) {
+ // Server responded with error status
+ console.error("API Error:", error.response.data);
+ } else if (error.request) {
+ // Request made but no response
+ console.error("Network Error:", error.message);
+ } else {
+ // Something else happened
+ console.error("Error:", error.message);
+ }
+ return Promise.reject(error);
+ }
+);
+
+export default api;
diff --git a/client/src/services/messageService.js b/client/src/services/messageService.js
new file mode 100644
index 000000000..2b4ead424
--- /dev/null
+++ b/client/src/services/messageService.js
@@ -0,0 +1,35 @@
+import api from "./api";
+
+// Get all conversations for a user
+export const getUserConversations = async (userId) => {
+ const response = await api.get(`/messages/conversations/${userId}`);
+ return response.data;
+};
+
+// Get or create conversation between two users
+export const getOrCreateConversation = async (user1Id, user2Id) => {
+ const response = await api.post("/messages/conversations", { user1Id, user2Id });
+ return response.data;
+};
+
+// Get messages in a conversation
+export const getConversationMessages = async (conversationId) => {
+ const response = await api.get(`/messages/conversations/${conversationId}/messages`);
+ return response.data;
+};
+
+// Send a message
+export const sendMessage = async (conversationId, senderId, content) => {
+ const response = await api.post("/messages/send", {
+ conversation_id: conversationId,
+ sender_id: senderId,
+ content,
+ });
+ return response.data;
+};
+
+// Mark messages as read
+export const markMessagesAsRead = async (conversationId, userId) => {
+ const response = await api.post("/messages/read", { conversationId, userId });
+ return response.data;
+};
diff --git a/client/src/services/postService.js b/client/src/services/postService.js
new file mode 100644
index 000000000..6011c7468
--- /dev/null
+++ b/client/src/services/postService.js
@@ -0,0 +1,31 @@
+import api from "./api";
+
+// Get all posts
+export const getAllPosts = async () => {
+ const response = await api.get("/posts");
+ return response.data;
+};
+
+// Get post by ID
+export const getPostById = async (id) => {
+ const response = await api.get(`/posts/${id}`);
+ return response.data;
+};
+
+// Create a new post
+export const createPost = async (postData) => {
+ const response = await api.post("/posts", postData);
+ return response.data;
+};
+
+// Update a post
+export const updatePost = async (id, postData) => {
+ const response = await api.put(`/posts/${id}`, postData);
+ return response.data;
+};
+
+// Delete a post
+export const deletePost = async (id) => {
+ const response = await api.delete(`/posts/${id}`);
+ return response.data;
+};
diff --git a/client/src/services/userService.js b/client/src/services/userService.js
new file mode 100644
index 000000000..a5004996c
--- /dev/null
+++ b/client/src/services/userService.js
@@ -0,0 +1,55 @@
+import api from "./api";
+
+// Get all users
+export const getAllUsers = async () => {
+ const response = await api.get("/users");
+ return response.data;
+};
+
+// Get user by ID
+export const getUserById = async (id) => {
+ const response = await api.get(`/users/${id}`);
+ return response.data;
+};
+
+// Get user by username
+export const getUserByUsername = async (username) => {
+ const response = await api.get(`/users/username/${username}`);
+ return response.data;
+};
+
+// Create a new user
+export const createUser = async (userData) => {
+ const response = await api.post("/users", userData);
+ return response.data;
+};
+
+// Update a user
+export const updateUser = async (id, userData) => {
+ const response = await api.put(`/users/${id}`, userData);
+ return response.data;
+};
+
+// Delete a user
+export const deleteUser = async (id) => {
+ const response = await api.delete(`/users/${id}`);
+ return response.data;
+};
+
+// Add friend
+export const addFriend = async (userId, friendId) => {
+ const response = await api.post("/users/friends/add", { userId, friendId });
+ return response.data;
+};
+
+// Remove friend
+export const removeFriend = async (userId, friendId) => {
+ const response = await api.post("/users/friends/remove", { userId, friendId });
+ return response.data;
+};
+
+// Get user's friends
+export const getFriends = async (userId) => {
+ const response = await api.get(`/users/${userId}/friends`);
+ return response.data;
+};
diff --git a/client/src/supabaseClient.js b/client/src/supabaseClient.js
new file mode 100644
index 000000000..0f04190dc
--- /dev/null
+++ b/client/src/supabaseClient.js
@@ -0,0 +1,6 @@
+import { createClient } from "@supabase/supabase-js";
+
+const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+export const supabase = createClient(supabaseUrl, supabaseAnonKey);
\ No newline at end of file
diff --git a/client/vite.config.js b/client/vite.config.js
new file mode 100644
index 000000000..910adb42a
--- /dev/null
+++ b/client/vite.config.js
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ },
+ },
+ },
+})
diff --git a/documentation/MESSAGING_SYSTEM.md b/documentation/MESSAGING_SYSTEM.md
new file mode 100644
index 000000000..f7f0a1b40
--- /dev/null
+++ b/documentation/MESSAGING_SYSTEM.md
@@ -0,0 +1,238 @@
+# Messaging System Documentation
+
+## Overview
+
+The Lexington Links messaging system allows users to send direct messages to their friends in real-time. Users can only message people they have added as friends (stored in their `follows_ids`).
+
+---
+
+## Database Schema
+
+### Table: `conversations`
+Stores one-on-one chat conversations between two users.
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | integer | Unique conversation ID (auto-generated) |
+| `created_at` | timestamp | When the conversation was created |
+| `user_id_1` | integer | ID of first participant (always smaller) |
+| `user_id_2` | integer | ID of second participant (always larger) |
+| `last_message_at` | timestamp | Timestamp of the most recent message |
+
+**Key Points:**
+- `user_id_1` is always less than `user_id_2` to prevent duplicate conversations
+- Unique constraint ensures only one conversation exists between two users
+
+### Table: `messages`
+Stores individual messages within conversations.
+
+| Column | Type | Description |
+|--------|------|-------------|
+| `id` | integer | Unique message ID (auto-generated) |
+| `created_at` | timestamp | When the message was sent |
+| `conversation_id` | integer | References `conversations.id` |
+| `sender_id` | integer | User ID of who sent the message |
+| `content` | text | The actual message text |
+| `read` | boolean | Whether the message has been read (default: false) |
+
+**Key Points:**
+- Foreign key to `conversations` with CASCADE delete
+- Messages are deleted if their conversation is deleted
+
+---
+
+## How It Works
+
+### 1. Starting a Conversation
+
+**User Journey:**
+1. User clicks "+ New Chat" on the Messages page
+2. A list of their friends appears
+3. User clicks on a friend to start chatting
+
+**What Happens:**
+```javascript
+// Frontend calls:
+getOrCreateConversation(user.id, friend.id)
+
+// Backend checks if conversation exists between these users
+// If not, creates new conversation with user_id_1 < user_id_2
+```
+
+### 2. Sending Messages
+
+**User Journey:**
+1. User types message in the chat window
+2. Clicks "Send" button
+3. Message appears instantly in the chat
+
+**What Happens:**
+```javascript
+// Frontend calls:
+sendMessage(conversationId, senderId, content)
+
+// Backend:
+// 1. Inserts message into messages table
+// 2. Updates conversation's last_message_at timestamp
+```
+
+### 3. Viewing Conversations
+
+**User Journey:**
+1. User navigates to `/messages`
+2. Sees list of all their conversations (sorted by most recent)
+3. Each conversation shows the other person's name and profile picture
+
+**What Happens:**
+```javascript
+// Frontend calls:
+getUserConversations(userId)
+
+// Backend:
+// 1. Finds all conversations where user is participant
+// 2. For each conversation, fetches the OTHER user's info
+// 3. Returns conversations sorted by last_message_at (newest first)
+```
+
+### 4. Reading Messages
+
+**User Journey:**
+1. User clicks on a conversation
+2. All messages in that conversation load
+3. Messages are automatically marked as read
+
+**What Happens:**
+```javascript
+// Frontend calls:
+getConversationMessages(conversationId)
+markMessagesAsRead(conversationId, userId)
+
+// Backend:
+// 1. Fetches all messages for that conversation (oldest first)
+// 2. Marks unread messages (where sender ≠ current user) as read
+```
+
+---
+
+## API Endpoints
+
+### Get User's Conversations
+```
+GET /api/messages/conversations/:userId
+```
+Returns all conversations for a user with the other participant's info.
+
+### Create or Get Conversation
+```
+POST /api/messages/conversations
+Body: { user1Id, user2Id }
+```
+Finds existing conversation or creates a new one.
+
+### Get Conversation Messages
+```
+GET /api/messages/conversations/:conversationId/messages
+```
+Returns all messages in a conversation (chronological order).
+
+### Send Message
+```
+POST /api/messages/send
+Body: { conversation_id, sender_id, content }
+```
+Sends a new message and updates conversation timestamp.
+
+### Mark as Read
+```
+POST /api/messages/read
+Body: { conversationId, userId }
+```
+Marks all unread messages in a conversation as read.
+
+---
+
+## Frontend Components
+
+### Messages Page (`/messages`)
+
+**Layout:**
+- **Left Sidebar:** List of conversations
+- **Right Panel:** Active chat window
+
+**Features:**
+- "+ New Chat" button to start new conversations
+- Shows friend's profile picture and name
+- Auto-scrolls to newest message
+- Real-time message sending
+- Timestamps for all messages
+
+**State Management:**
+```javascript
+conversations // List of all user's conversations
+selectedConversation // Currently active conversation
+messages // Messages in selected conversation
+friends // User's friends list
+newMessage // Current message being typed
+```
+
+---
+
+## Friend Restriction
+
+Users can **only message people they follow**. This is enforced by:
+
+1. **Friend List:** Only friends appear in "+ New Chat" popup
+2. **Backend Logic:** Conversations are created between any two users (no restriction)
+3. **Frontend UX:** Users can only discover/start chats with friends
+
+**To add friends:**
+1. Go to User Lookup page
+2. Search for username
+3. Click "Add Friend"
+
+---
+
+## Example Flow
+
+**Alice wants to message Bob:**
+
+1. Alice goes to `/messages`
+2. Clicks "+ New Chat"
+3. Sees Bob in her friends list (she added him earlier)
+4. Clicks Bob's name
+5. System creates conversation with `user_id_1=1 (Alice), user_id_2=2 (Bob)`
+6. Alice types "Hey Bob!"
+7. Message is inserted: `{conversation_id: 1, sender_id: 1, content: "Hey Bob!"}`
+8. Bob sees the conversation appear in his messages list
+9. Bob opens it and replies
+10. Both see the full conversation history
+
+---
+
+## File Structure
+
+```
+server/
+ controllers/messages.js # Backend logic
+ routes/messages.js # API routes
+ tables/
+ setup_messaging.sql # Database setup script
+
+client/
+ src/
+ pages/Messages.jsx # Main messaging UI
+ services/messageService.js # API calls
+```
+
+---
+
+## Future Enhancements
+
+Possible features to add:
+- Real-time updates (WebSockets)
+- Message editing/deletion
+- Image/file attachments
+- Group messaging
+- Typing indicators
+- Message reactions
+- Search within conversations
diff --git a/documentation/USER_TABLE_MERGE.md b/documentation/USER_TABLE_MERGE.md
new file mode 100644
index 000000000..ad04535ec
--- /dev/null
+++ b/documentation/USER_TABLE_MERGE.md
@@ -0,0 +1,254 @@
+# User Table Merge - Implementation Guide
+
+## Problem
+- Two overlapping tables: `user` (your code) and `profiles` (James's code)
+- Caused conflicts and duplicate data
+- `user` table had integer IDs, incompatible with Supabase Auth UUIDs
+
+## Solution
+Merged both tables into a single **`user`** table with the best fields from both.
+
+---
+
+## Unified User Table Schema
+
+```sql
+CREATE TABLE "user" (
+ -- Core Identity
+ id UUID PRIMARY KEY, -- From Supabase Auth
+ created_at TIMESTAMP,
+ updated_at TIMESTAMP,
+
+ -- Profile Info
+ username TEXT UNIQUE, -- Required, unique
+ display_name TEXT, -- Optional display name
+ pfp TEXT, -- Profile picture URL
+ bio TEXT, -- User bio
+
+ -- School Info
+ borough TEXT, -- User's NYC borough
+ year TEXT, -- Freshman/Sophomore/Junior/Senior
+
+ -- Social Data
+ interests JSONB DEFAULT '{}', -- {"activity": true}
+ follows_ids JSONB DEFAULT '{}', -- {"friend_uuid": true}
+
+ -- Legacy (can remove later if unused)
+ post_ids JSONB,
+ group_ids JSONB,
+ message_ids JSONB
+);
+```
+
+---
+
+## Migration Steps
+
+### 1. Run SQL Migration in Supabase
+Go to Supabase SQL Editor and run:
+```
+server/tables/FINAL_USER_TABLE_MIGRATION.sql
+```
+
+This will:
+- ✅ Drop old `user` and `profiles` tables
+- ✅ Create new unified `user` table with UUID IDs
+- ✅ Recreate `conversations` and `messages` tables with UUID foreign keys
+- ✅ Set up Row Level Security (RLS) policies
+- ✅ Create indexes for performance
+
+**⚠️ WARNING:** This deletes all existing data! Export first if needed.
+
+### 2. Code Already Updated
+All code has been updated to use the merged schema:
+
+**Frontend:**
+- ✅ `SignUp.jsx` - Creates users with all fields (username, display_name, bio, borough, year, interests)
+- ✅ `AuthContext.jsx` - Handles UUID auth IDs, creates users with merged schema
+- ✅ `Login.jsx` - Uses AuthContext
+- ✅ `Profile.jsx` - Uses unified `user` table (not `profiles`)
+- ✅ `Home.jsx` - Already uses `user` table
+- ✅ `Messages.jsx` - Already uses `user` table
+- ✅ `UserLookup.jsx` - Already uses `user` table
+
+**Backend:**
+- ✅ `controllers/messages.js` - Updated to handle UUID strings (not integers)
+- ✅ `controllers/user.js` - Already uses `user` table
+- ✅ `controllers/post.js` - Already uses `user` table
+
+---
+
+## What Changed
+
+### Field Name Mappings
+
+| Old (`profiles`) | Old (`user`) | New (merged) |
+|-----------------|--------------|--------------|
+| pfp_url | pfp | **pfp** |
+| display_name | - | **display_name** |
+| bio | - | **bio** |
+| borough | - | **borough** |
+| year | - | **year** |
+| interests (array) | interests (jsonb) | **interests** (jsonb) |
+| - | follows_ids | **follows_ids** |
+
+### UUID Changes
+
+**Before:**
+- `user.id` was `integer` (auto-increment)
+- Supabase Auth generated UUIDs like `"6fd12224-1339-43d6-876a-884450807363"`
+- **Incompatible!** ❌
+
+**After:**
+- `user.id` is `uuid` (matches Supabase Auth)
+- All foreign keys updated: `conversations.user_id_1`, `conversations.user_id_2`, `messages.sender_id`
+- **Compatible!** ✅
+
+---
+
+## How It Works Now
+
+### 1. Sign Up Flow
+```javascript
+// User signs up
+supabase.auth.signUp({ email, password })
+
+// Creates user in 'user' table with Supabase Auth UUID
+supabase.from("user").insert({
+ id: authUser.id, // UUID from auth
+ username: formData.username,
+ display_name: formData.displayName,
+ borough: formData.borough,
+ year: formData.year,
+ interests: {"hiking": true, "coding": true},
+ follows_ids: {}
+})
+```
+
+### 2. Login Flow
+```javascript
+// User logs in
+supabase.auth.signInWithPassword({ email, password })
+
+// AuthContext automatically loads user from 'user' table
+const { data: userData } = await supabase
+ .from("user")
+ .select("*")
+ .eq("id", authUser.id)
+```
+
+### 3. Creating Posts
+```javascript
+// Uses real authenticated user UUID
+await createPost({
+ user_id: user.id, // UUID like "6fd12224-..."
+ caption: "Hello!",
+ content: "My first post"
+})
+```
+
+### 4. Adding Friends
+```javascript
+// Updates follows_ids with friend's UUID
+await supabase
+ .from("user")
+ .update({
+ follows_ids: {
+ ...currentFollows,
+ "friend-uuid-here": true
+ }
+ })
+ .eq("id", user.id)
+```
+
+### 5. Messaging
+```javascript
+// Creates conversation with two UUIDs
+await supabase
+ .from("conversations")
+ .insert({
+ user_id_1: "uuid-1", // Alphabetically first
+ user_id_2: "uuid-2" // Alphabetically second
+ })
+```
+
+---
+
+## Testing Checklist
+
+After running the migration:
+
+- [ ] Sign up a new user → Check `user` table has UUID id
+- [ ] Log in → User loads correctly
+- [ ] Create a post → Uses UUID user_id
+- [ ] View profile → Shows all fields (username, display_name, bio, etc.)
+- [ ] Edit profile → Updates correctly
+- [ ] Search for user → Find by username
+- [ ] Add friend → Updates follows_ids
+- [ ] Send message → Creates conversation with UUIDs
+- [ ] Log out → Clears auth state
+- [ ] Try accessing protected route while logged out → Redirects to login
+
+---
+
+## Row Level Security (RLS)
+
+The migration enables RLS with these policies:
+
+### User Table
+- ✅ **Anyone can view** all profiles (for user lookup, friends list)
+- ✅ **Users can only update** their own profile
+- ✅ **Users can only insert** their own profile (during signup)
+
+### Conversations
+- ✅ Users can only see conversations they're part of
+- ✅ Users can only create conversations with themselves as a participant
+
+### Messages
+- ✅ Users can only see messages in their own conversations
+- ✅ Users can only send messages in their own conversations
+- ✅ Users can only mark messages as read in their own conversations
+
+---
+
+## Benefits
+
+1. **Single Source of Truth** - No more conflicts between `user` and `profiles`
+2. **Supabase Auth Compatible** - UUID IDs work seamlessly
+3. **All Features Available** - Has fields from both old tables
+4. **Secure** - RLS policies protect user data
+5. **Performant** - Indexed fields for fast queries
+6. **Automatic Timestamps** - `updated_at` auto-updates on changes
+
+---
+
+## If You Need to Preserve Existing Data
+
+If you have important data in the current tables:
+
+1. **Export current data:**
+ ```sql
+ -- In Supabase SQL Editor
+ SELECT * FROM "user";
+ SELECT * FROM profiles;
+ ```
+
+2. **Save as CSV** (Supabase has export button)
+
+3. **Run migration** (creates new empty tables)
+
+4. **Import data** with UUID conversion:
+ ```sql
+ -- You'll need to manually map integer IDs to new UUIDs
+ -- Or let users re-signup with same emails
+ ```
+
+---
+
+## Future Improvements
+
+- Remove legacy fields (`post_ids`, `group_ids`, `message_ids`) if unused
+- Add more profile fields (avatar customization, theme preferences, etc.)
+- Add email verification requirement
+- Add password reset functionality
+- Add social login (Google, GitHub, etc.)
diff --git a/milestones/milestone1.md b/milestones/milestone1.md
deleted file mode 100644
index 52b9b0038..000000000
--- a/milestones/milestone1.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Milestone 1
-
-This document should be completed and submitted during **Unit 5** of this course. You **must** check off all completed tasks in this document in order to receive credit for your work.
-
-## Checklist
-
-This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-
-- [ ] Read and understand all required features
- - [ ] Understand you **must** implement **all** baseline features and **two** custom features
-- [ ] In `readme.md`: update app name to your app's name
-- [ ] In `readme.md`: add all group members' names
-- [ ] In `readme.md`: complete the **Description and Purpose** section
-- [ ] In `readme.md`: complete the **Inspiration** section
-- [ ] In `readme.md`: list a name and description for all features (minimum 6 for full points) you intend to include in your app (in future units, you will check off features as you complete them and add GIFs demonstrating the features)
-- [ ] In `planning/user_stories.md`: add all user stories (minimum 10 for full points)
-- [ ] In `planning/user_stories.md`: use 1-3 unique user roles in your user stories
-- [ ] In this document, complete all thre questions in the **Reflection** section below
-
-## Reflection
-
-### 1. What went well during this unit?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 2. What were some challenges your group faced in this unit?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 3. What additional support will you need in upcoming units as you continue to work on your final project?
-
-[👉🏾👉🏾👉🏾 your answer here]
diff --git a/milestones/milestone2.md b/milestones/milestone2.md
deleted file mode 100644
index e3178cd81..000000000
--- a/milestones/milestone2.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Milestone 2
-
-This document should be completed and submitted during **Unit 6** of this course. You **must** check off all completed tasks in this document in order to receive credit for your work.
-
-## Checklist
-
-This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-
-- [ ] In `planning/wireframes.md`: add wireframes for at least three pages in your web app.
- - [ ] Include a list of pages in your app
-- [ ] In `planning/entity_relationship_diagram.md`: add the entity relationship diagram you developed for your database.
- - [ ] Your entity relationship diagram should include the tables in your database.
-- [ ] Prepare your three-minute pitch presentation, to be presented during Unit 7 (the next unit).
- - [ ] You do **not** need to submit any materials in advance of your pitch.
-- [ ] In this document, complete all three questions in the **Reflection** section below
-
-## Reflection
-
-### 1. What went well during this unit?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 2. What were some challenges your group faced in this unit?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 3. What additional support will you need in upcoming units as you continue to work on your final project?
-
-[👉🏾👉🏾👉🏾 your answer here]
diff --git a/milestones/milestone3.md b/milestones/milestone3.md
deleted file mode 100644
index 571ce7651..000000000
--- a/milestones/milestone3.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# Milestone 3
-
-This document should be completed and submitted during **Unit 7** of this course. You **must** check off all completed tasks in this document in order to receive credit for your work.
-
-## Checklist
-
-This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-
-You will need to reference the GitHub Project Management guide in the course portal for more information about how to complete each of these steps.
-
-- [ ] In your repo, create a project board.
- - *Please be sure to share your project board with the grading team's GitHub **codepathreview**. This is separate from your repository's sharing settings.*
-- [ ] In your repo, create at least 5 issues from the features on your feature list.
-- [ ] In your repo, update the status of issues in your project board.
-- [ ] In your repo, create a GitHub Milestone for each final project unit, corresponding to each of the 5 milestones in your `milestones/` directory.
- - [ ] Set the completion percentage of each milestone. The GitHub Milestone for this unit (Milestone 3 - Unit 7) should be 100% completed when you submit for full points.
-- [ ] In `readme.md`, check off the features you have completed in this unit by adding a ✅ emoji in front of the feature's name.
- - [ ] Under each feature you have completed, include a GIF showing feature functionality.
-- [ ] In this documents, complete all five questions in the **Reflection** section below.
-
-## Reflection
-
-### 1. What went well during this unit?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 2. What were some challenges your group faced in this unit?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### Did you finish all of your tasks in your sprint plan for this week? If you did not finish all of the planned tasks, how would you prioritize the remaining tasks on your list?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### Which features and user stories would you consider “at risk”? How will you change your plan if those items remain “at risk”?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 5. What additional support will you need in upcoming units as you continue to work on your final project?
-
-[👉🏾👉🏾👉🏾 your answer here]
diff --git a/milestones/milestone5.md b/milestones/milestone5.md
deleted file mode 100644
index 139284283..000000000
--- a/milestones/milestone5.md
+++ /dev/null
@@ -1,105 +0,0 @@
-# Milestone 5
-
-This document should be completed and submitted during **Unit 9** of this course. You **must** check off all completed tasks in this document in order to receive credit for your work.
-
-## Checklist
-
-This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
-
-- [ ] Deploy your project on Render
- - [ ] In `readme.md`, add the link to your deployed project
-- [ ] Update the status of issues in your project board as you complete them
-- [ ] In `readme.md`, check off the features you have completed in this unit by adding a ✅ emoji in front of their title
- - [ ] Under each feature you have completed, **include a GIF** showing feature functionality
-- [ ] In this document, complete the **Reflection** section below
-- [ ] 🚩🚩🚩**Complete the Final Project Feature Checklist section below**, detailing each feature you completed in the project (ONLY include features you implemented, not features you planned)
-- [ ] 🚩🚩🚩**Record a GIF showing a complete run-through of your app** that displays all the components included in the **Final Project Feature Checklist** below
- - [ ] Include this GIF in the **Final Demo GIF** section below
-
-## Final Project Feature Checklist
-
-Complete the checklist below detailing each baseline, custom, and stretch feature you completed in your project. This checklist will help graders look for each feature in the GIF you submit.
-
-### Baseline Features
-
-👉🏾👉🏾👉🏾 Check off each completed feature below.
-
-- [ ] The project includes an Express backend app and a React frontend app
-- [ ] The project includes these backend-specific features:
- - [ ] At least one of each of the following database relationships in Postgres
- - [ ] one-to-many
- - [ ] many-to-many with a join table
- - [ ] A well-designed RESTful API that:
- - [ ] supports all four main request types for a single entity (ex. tasks in a to-do list app): GET, POST, PATCH, and DELETE
- - [ ] the user can **view** items, such as tasks
- - [ ] the user can **create** a new item, such as a task
- - [ ] the user can **update** an existing item by changing some or all of its values, such as changing the title of task
- - [ ] the user can **delete** an existing item, such as a task
- - [ ] Routes follow proper naming conventions
- - [ ] The web app includes the ability to reset the database to its default state
-- [ ] The project includes these frontend-specific features:
- - [ ] At least one redirection, where users are able to navigate to a new page with a new URL within the app
- - [ ] At least one interaction that the user can initiate and complete on the same page without navigating to a new page
- - [ ] Dynamic frontend routes created with React Router
- - [ ] Hierarchically designed React components
- - [ ] Components broken down into categories, including Page and Component types
- - [ ] Corresponding container components and presenter components as appropriate
-- [ ] The project includes dynamic routes for both frontend and backend apps
-- [ ] The project is deployed on Render with all pages and features that are visible to the user are working as intended
-
-### Custom Features
-
-👉🏾👉🏾👉🏾 Check off each completed feature below.
-
-- [ ] The project gracefully handles errors
-- [ ] The project includes a one-to-one database relationship
-- [ ] The project includes a slide-out pane or modal as appropriate for your use case that pops up and covers the page content without navigating away from the current page
-- [ ] The project includes a unique field within the join table
-- [ ] The project includes a custom non-RESTful route with corresponding controller actions
-- [ ] The user can filter or sort items based on particular criteria as appropriate for your use case
-- [ ] Data is automatically generated in response to a certain event or user action. Examples include generating a default inventory for a new user starting a game or creating a starter set of tasks for a user creating a new task app account
-- [ ] Data submitted via a POST or PATCH request is validated before the database is updated (e.g. validating that an event is in the future before allowing a new event to be created)
- - [ ] *To receive full credit, please be sure to demonstrate in your walkthrough that for certain inputs, the item will NOT be successfully created or updated.*
-
-### Stretch Features
-
-👉🏾👉🏾👉🏾 Check off each completed feature below.
-
-- [ ] A subset of pages require the user to log in before accessing the content
- - [ ] Users can log in and log out via GitHub OAuth with Passport.js
-- [ ] Restrict available user options dynamically, such as restricting available purchases based on a user's currency
-- [ ] Show a spinner while a page or page element is loading
-- [ ] Disable buttons and inputs during the form submission process
-- [ ] Disable buttons after they have been clicked
- - *At least 75% of buttons in your app must exhibit this behavior to receive full credit*
-- [ ] Users can upload images to the app and have them be stored on a cloud service
- - *A user profile picture does **NOT** count for this rubric item **only if** the app also includes "Login via GitHub" functionality.*
- - *Adding a photo via a URL does **NOT** count for this rubric item (for example, if the user provides a URL with an image to attach it to the post).*
- - *Selecting a photo from a list of provided photos does **NOT** count for this rubric item.*
-- [ ] 🍞 [Toast messages](https://www.patternfly.org/v3/pattern-library/communication/toast-notifications/index.html) deliver simple feedback in response to user events
-
-## Final Demo GIF
-
-🔗 [Here's a GIF walkthrough of the final project](👉🏾👉🏾👉🏾 your link here)
-
-## Reflection
-
-### 1. What went well during this unit?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 2. What were some challenges your group faced in this unit?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 3. What were some of the highlights or achievements that you are most proud of in this project?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 4. Reflecting on your web development journey so far, how have you grown since the beginning of the course?
-
-[👉🏾👉🏾👉🏾 your answer here]
-
-### 5. Looking ahead, what are your goals related to web development, and what steps do you plan to take to achieve them?
-
-[👉🏾👉🏾👉🏾 your answer here]
diff --git a/planning/entity_relationship_diagram.md b/planning/entity_relationship_diagram.md
deleted file mode 100644
index 12c25f62c..000000000
--- a/planning/entity_relationship_diagram.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# Entity Relationship Diagram
-
-Reference the Creating an Entity Relationship Diagram final project guide in the course portal for more information about how to complete this deliverable.
-
-## Create the List of Tables
-
-[👉🏾👉🏾👉🏾 List each table in your diagram]
-
-## Add the Entity Relationship Diagram
-
-[👉🏾👉🏾👉🏾 Include an image or images of the diagram below. You may also wish to use the following markdown syntax to outline each table, as per your preference.]
-
-| Column Name | Type | Description |
-|-------------|------|-------------|
-| id | integer | primary key |
-| name | text | name of the shoe model |
-| ... | ... | ... |
diff --git a/planning/user_stories.md b/planning/user_stories.md
deleted file mode 100644
index 1e55ecbcd..000000000
--- a/planning/user_stories.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# User Stories
-
-Reference the Writing User Stories final project guide in the course portal for more information about how to complete each of the sections below.
-
-## Outline User Roles
-
-[👉🏾👉🏾👉🏾 Include at least at least 1, but no more than 3, user roles.]
-
-## Draft User Stories
-
-[👉🏾👉🏾👉🏾 Include at least at least 10 user stories in this format:]
-
-1. As a [user role], I want to [what], so that [why].
diff --git a/project_details/milestones/milestone1.md b/project_details/milestones/milestone1.md
new file mode 100644
index 000000000..2bd3b150d
--- /dev/null
+++ b/project_details/milestones/milestone1.md
@@ -0,0 +1,32 @@
+# Milestone 1
+
+This document should be completed and submitted during **Unit 5** of this course. You **must** check off all completed tasks in this document in order to receive credit for your work.
+
+## Checklist
+
+This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
+
+- [X] Read and understand all required features
+ - [X] Understand you **must** implement **all** baseline features and **two** custom features
+- [X] In `readme.md`: update app name to your app's name
+- [X] In `readme.md`: add all group members' names
+- [X] In `readme.md`: complete the **Description and Purpose** section
+- [X] In `readme.md`: complete the **Inspiration** section
+- [X] In `readme.md`: list a name and description for all features (minimum 6 for full points) you intend to include in your app (in future units, you will check off features as you complete them and add GIFs demonstrating the features)
+- [X] In `planning/user_stories.md`: add all user stories (minimum 10 for full points)
+- [X] In `planning/user_stories.md`: use 1-3 unique user roles in your user stories
+- [X] In this document, complete all thre questions in the **Reflection** section below
+
+## Reflection
+
+### 1. What went well during this unit?
+
+This unit's success centered on establishing a strong foundation for our web application. We effectively collaborated to choose a unique concept, which we documented in the readme.md, along with the names of all group members. We successfully developed more than the required ten detailed user stories across multiple user roles, which clearly defined the core functionality and user journeys. Finally, we created a comprehensive initial feature list, ensuring all baseline features and our planned two custom features are accounted for, putting us in a great position to start the design phase in Milestone 2.
+
+### 2. What were some challenges your group faced in this unit?
+
+Given that our project is a dating app, the main challenge we faced in Milestone 1 was scoping the project's core functionality against the required database features. Specifically, we spent significant time mapping the complex many-to-many relationship (i.e., how two users become a "Match") and ensuring this was accurately represented in our initial set of user stories. This required us to carefully define the boundaries for our two custom features—such as deciding which kind of filtering or validation would add the most value without becoming overly complex—to ensure we could still meet all the required baseline features while maintaining a manageable workload for the upcoming development units.
+
+### 3. What additional support will you need in upcoming units as you continue to work on your final project?
+
+As we transition into the development phases (Milestones 3 and Final), our group anticipates needing the most support with configuring our relational data for the Express server and integrating our full-stack deployment. For the dating app, the core logic relies on complex Postgres queries that efficiently handle the required text many-to-many "Match" relationship and custom user filtering. We'll need guidance from TFs or instructors on best practices for writing these SQL queries within our Node/Express controllers, specifically around preventing performance bottlenecks. Furthermore, we expect to require assistance during the final unit to successfully deploy the separate React frontend and Express backend to Render, ensuring proper CORS configuration and correct handling of React Router dynamic routes on the live service.
\ No newline at end of file
diff --git a/project_details/milestones/milestone2.md b/project_details/milestones/milestone2.md
new file mode 100644
index 000000000..3c96af12d
--- /dev/null
+++ b/project_details/milestones/milestone2.md
@@ -0,0 +1,34 @@
+# Milestone 2
+
+This document should be completed and submitted during **Unit 6** of this course. You **must** check off all completed tasks in this document in order to receive credit for your work.
+
+## Checklist
+
+This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
+
+- [X] In `planning/wireframes.md`: add wireframes for at least three pages in your web app.
+ - [X] Include a list of pages in your app
+- [X] In `planning/entity_relationship_diagram.md`: add the entity relationship diagram you developed for your database.
+ - [X] Your entity relationship diagram should include the tables in your database.
+- [X] Prepare your three-minute pitch presentation, to be presented during Unit 7 (the next unit).
+ - [X] You do **not** need to submit any materials in advance of your pitch.
+- [X] In this document, complete all three questions in the **Reflection** section below
+
+## Reflection
+
+### 1. What went well during this unit?
+
+The transition from abstract concepts (Milestone 1) to concrete blueprints (Milestone 2) was highly productive. What went best was the collaboration on the Entity Relationship Diagram (ERD). We successfully mapped the complex relationships required for our app's core functionality, specifically:
+* Many-to-Many: We clearly defined the User Group and User text Course relationships, which are critical for both the search and group formation features.
+* One-to-One: We successfully integrated the planned User Block List as a 1:1 relationship, satisfying a Custom Feature requirement early in the planning process.
+* Pitch Preparation: Preparing the three-minute pitch was also efficient. The clarity gained from the ERD and wireframes allowed us to concisely articulate the app's value proposition and technical complexity, making our presentation preparation time very focused.
+
+### 2. What were some challenges your group faced in this unit?
+
+Our main challenge centered on wireframing the key user interactions and ensuring they were intuitive for a commuter college environment.
+* Mapping the Swipe Feature: Translating the "Swipe Functionality" user story into a clean wireframe required several revisions. We struggled initially to clearly indicate how a user would swipe (a required same-page interaction) versus how they would filter their results, which is key to finding the right study buddies. We resolved this by including clear Filter and Settings icons on the discovery page.
+* Group Scheduling Logic: Designing the flow for the "Group formation" feature was complex, particularly deciding where to place the logic for scheduling hangout plans within the wireframes (e.g., as a slide-out modal vs. a separate page). This required us to review the ERD and chat features concurrently to ensure the flow made sense before committing to the final wireframe design.
+
+### 3. What additional support will you need in upcoming units as you continue to work on your final project?
+
+Complex Postgres Querying: Our app relies heavily on filtering (by course, major, time) and complex join operations to display accurate match results and group rosters. We anticipate needing focused guidance or examples on writing efficient SQL queries that utilize the many-to-many join tables without creating performance bottlenecks.Custom Non-RESTful Route: We are planning a custom, non-RESTful route for handling the specific action of a user liking a profile (a successful swipe), and would appreciate a review of the controller and Express route structure for this specific type of endpoint before implementation.
diff --git a/project_details/milestones/milestone3.md b/project_details/milestones/milestone3.md
new file mode 100644
index 000000000..fe84d690f
--- /dev/null
+++ b/project_details/milestones/milestone3.md
@@ -0,0 +1,41 @@
+# Milestone 3
+
+This document should be completed and submitted during **Unit 7** of this course. You **must** check off all completed tasks in this document in order to receive credit for your work.
+
+## Checklist
+
+This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
+
+You will need to reference the GitHub Project Management guide in the course portal for more information about how to complete each of these steps.
+
+- [✅] In your repo, create a project board.
+ - _Please be sure to share your project board with the grading team's GitHub **codepathreview**. This is separate from your repository's sharing settings._
+- [✅ ] In your repo, create at least 5 issues from the features on your feature list.
+- [✅] In your repo, update the status of issues in your project board.
+- [✅] In your repo, create a GitHub Milestone for each final project unit, corresponding to each of the 5 milestones in your `milestones/` directory.
+ - [✅] Set the completion percentage of each milestone. The GitHub Milestone for this unit (Milestone 3 - Unit 7) should be 100% completed when you submit for full points.
+- [✅] In `readme.md`, check off the features you have completed in this unit by adding a ✅ emoji in front of the feature's name.
+ - [✅] Under each feature you have completed, include a GIF showing feature functionality.
+- [✅] In this documents, complete all five questions in the **Reflection** section below.
+
+## Reflection
+
+### 1. What went well during this unit?
+
+The frontend development went well this unit. We successfully implemented a beautiful, multi-step signup form with interactive features like Tinder-style swipeable activity cards and an SVG-based interactive NYC borough map. The home page was also redesigned with a stylistic scrolling layout that transitions from a purple "Suggested Groups" section to a dark gray posts feed, creating a polished and engaging user experience.
+
+### 2. What were some challenges your group faced in this unit?
+
+The main challenges involved fine-tuning the user interface elements to ensure they were both functional and visually appealing. Creating the interactive NYC map required careful positioning of SVG paths to prevent borough overlaps and ensure text labels fit properly within each shape. Additionally, implementing smooth animations and transitions while maintaining code cleanliness and performance required multiple iterations and refinements.
+
+### Did you finish all of your tasks in your sprint plan for this week? If you did not finish all of the planned tasks, how would you prioritize the remaining tasks on your list?
+
+We completed all major frontend layout and design tasks for the signup and home pages. Moving forward, the priority will be connecting the frontend to the backend API endpoints we created earlier, implementing actual authentication logic, and ensuring data flows correctly between the client and server. We'll also need to add form validation and error handling to make the user experience more robust.
+
+### Which features and user stories would you consider "at risk"? How will you change your plan if those items remain "at risk"?
+
+The "Brew Random Group" matching algorithm and real-time messaging features are currently at risk due to their complexity. If these remain at risk, we'll focus first on core CRUD operations for users, groups, and posts to ensure a functional MVP. We can implement a simplified group suggestion algorithm initially and add the more sophisticated matching logic in a later iteration. Real-time messaging could also be simplified to basic HTTP polling before implementing WebSockets if time becomes tight.
+
+### 5. What additional support will you need in upcoming units as you continue to work on your final project?
+
+We'll need guidance on best practices for integrating React with Supabase, particularly around authentication flows and real-time subscriptions. Support with deployment strategies would also be helpful as we'll need to deploy both the frontend and backend to production environments. Additionally, guidance on implementing secure API practices and handling edge cases in the matching algorithm would be valuable as we move into the backend integration phase.
diff --git a/milestones/milestone4.md b/project_details/milestones/milestone4.md
similarity index 100%
rename from milestones/milestone4.md
rename to project_details/milestones/milestone4.md
diff --git a/project_details/milestones/milestone5.md b/project_details/milestones/milestone5.md
new file mode 100644
index 000000000..addf8cc8e
--- /dev/null
+++ b/project_details/milestones/milestone5.md
@@ -0,0 +1,103 @@
+# Milestone 5
+
+This document should be completed and submitted during **Unit 9** of this course. You **must** check off all completed tasks in this document in order to receive credit for your work.
+
+## Checklist
+
+This unit, be sure to complete all tasks listed below. To complete a task, place an `x` between the brackets.
+
+- [ ] Deploy your project on Render
+ - [ ] In `readme.md`, add the link to your deployed project
+- [ ] Update the status of issues in your project board as you complete them
+- [ ] In `readme.md`, check off the features you have completed in this unit by adding a ✅ emoji in front of their title
+ - [ ] Under each feature you have completed, **include a GIF** showing feature functionality
+- [ ] In this document, complete the **Reflection** section below
+- [ ] 🚩🚩🚩**Complete the Final Project Feature Checklist section below**, detailing each feature you completed in the project (ONLY include features you implemented, not features you planned)
+- [ ] 🚩🚩🚩**Record a GIF showing a complete run-through of your app** that displays all the components included in the **Final Project Feature Checklist** below
+ - [ ] Include this GIF in the **Final Demo GIF** section below
+
+## Final Project Feature Checklist
+
+### Baseline Features
+
+👉🏾👉🏾👉🏾 Check off each completed feature below.
+
+- [✅] The project includes an Express backend app and a React frontend app
+- [✅] The project includes these backend-specific features:
+ - [✅] At least one of each of the following database relationships in Postgres
+ - [✅] one-to-many (user → posts, user → messages)
+ - [✅] many-to-many with a join table (users ↔ conversations via conversations table)
+ - [✅] A well-designed RESTful API that:
+ - [✅] supports all four main request types for a single entity (ex. tasks in a to-do list app): GET, POST, PATCH, and DELETE
+ - [✅] the user can **view** items, such as tasks (GET /api/users, GET /api/posts)
+ - [✅] the user can **create** a new item, such as a task (POST /api/users, POST /api/posts)
+ - [✅] the user can **update** an existing item by changing some or all of its values, such as changing the title of task (PATCH /api/users/:id)
+ - [✅] the user can **delete** an existing item, such as a task (DELETE /api/users/:id, DELETE /api/posts/:id)
+ - [✅] Routes follow proper naming conventions
+ - [ ] The web app includes the ability to reset the database to its default state
+- [✅] The project includes these frontend-specific features:
+ - [✅] At least one redirection, where users are able to navigate to a new page with a new URL within the app
+ - [✅] At least one interaction that the user can initiate and complete on the same page without navigating to a new page
+ - [✅] Dynamic frontend routes created with React Router
+ - [✅] Hierarchically designed React components
+ - [✅] Components broken down into categories, including Page and Component types
+ - [✅] Corresponding container components and presenter components as appropriate
+- [✅] The project includes dynamic routes for both frontend and backend apps
+- [ ] The project is deployed on Render with all pages and features that are visible to the user are working as intended
+
+### Custom Features
+
+👉🏾👉🏾👉🏾 Check off each completed feature below.
+
+- [✅] The project gracefully handles errors
+- [ ] The project includes a one-to-one database relationship
+- [ ] The project includes a slide-out pane or modal as appropriate for your use case that pops up and covers the page content without navigating away from the current page
+- [✅] The project includes a unique field within the join table (conversations table has last_message_at timestamp)
+- [✅] The project includes a custom non-RESTful route with corresponding controller actions (POST /api/users/addFriend, GET /api/users/:userId/friends)
+- [ ] The user can filter or sort items based on particular criteria as appropriate for your use case
+- [✅] Data is automatically generated in response to a certain event or user action. Examples include generating a default inventory for a new user starting a game or creating a starter set of tasks for a user creating a new task app account (Default user profile data created on signup with empty interests and follows_ids objects)
+- [ ] Data submitted via a POST or PATCH request is validated before the database is updated (e.g. validating that an event is in the future before allowing a new event to be created)
+ - [ ] *To receive full credit, please be sure to demonstrate in your walkthrough that for certain inputs, the item will NOT be successfully created or updated.*
+
+### Stretch Features
+
+👉🏾👉🏾👉🏾 Check off each completed feature below.
+
+- [✅] A subset of pages require the user to log in before accessing the content (All routes except /login and /signup are protected with ProtectedRoute wrapper)
+ - [ ] Users can log in and log out via GitHub OAuth with Passport.js
+- [✅] Restrict available user options dynamically, such as restricting available purchases based on a user's currency (Users cannot add themselves as friends; "Add Friend" button hidden for own profile)
+- [ ] Show a spinner while a page or page element is loading
+- [✅] Disable buttons and inputs during the form submission process (Loading states on signup, login, and add friend buttons)
+- [ ] Disable buttons after they have been clicked
+ - *At least 75% of buttons in your app must exhibit this behavior to receive full credit*
+- [ ] Users can upload images to the app and have them be stored on a cloud service
+ - *A user profile picture does **NOT** count for this rubric item **only if** the app also includes "Login via GitHub" functionality.*
+ - *Adding a photo via a URL does **NOT** count for this rubric item (for example, if the user provides a URL with an image to attach it to the post).*
+ - *Selecting a photo from a list of provided photos does **NOT** count for this rubric item.*
+- [ ] 🍞 [Toast messages](https://www.patternfly.org/v3/pattern-library/communication/toast-notifications/index.html) deliver simple feedback in response to user events
+
+## Final Demo GIF
+
+🔗 [Here's a GIF walkthrough of the final project](👉🏾👉🏾👉🏾 your link here)
+
+## Reflection
+
+### 1. What went well during this unit?
+
+Merging our branches went pretty smoothly once we figured out the workflow. The authentication features integrated nicely with our posts and messaging system. It took some back and forth to get the database schema sorted out, but once we had the UUID migration working everything clicked into place.
+
+### 2. What were some challenges your group faced in this unit?
+
+The biggest headache was definitely dealing with the user and profiles tables being set up differently because we had to completely rework the database to get them merged. Also, switching from regular number IDs to those UUID strings was way more work than I expected since it broke a bunch of foreign key relationships. Lastly, the Row Level Security in Supabase kept blocking our friend requests even though we thought we configured it correctly.
+
+### 3. What were some of the highlights or achievements that you are most proud of in this project?
+
+We're really proud of getting the database migration to work without breaking key features. The authentication flow with protected routes turned out really clean, where users get redirected to login if they're not signed in, which is exactly what we wanted. The messaging system is probably our favorite feature since it uses proper conversation structures and handles UUIDs correctly.
+
+### 4. Reflecting on your web development journey so far, how have you grown since the beginning of the course?
+
+We've learned a lot about how real authentication works, working first with Auth_Simulation and then to real authentication. Working with merge conflicts was time consuming at first but now we developed a good system to work with it. Building RESTful APIs makes way more sense now, and we're comfortable with setting up Express.js contexts and controllers.
+
+### 5. Looking ahead, what are your goals related to web development, and what steps do you plan to take to achieve them?
+
+Our main goal is to use web development as a way to showcase data projects with polished, interactive frontends. We want to get better at building visualizations and user interfaces that make complex data easy to understand and engaging to explore. This project taught us a lot about full-stack development, and we plan to apply those skills to create data-driven applications that look professional and function smoothly.
diff --git a/project_details/planning/entity_relations.png b/project_details/planning/entity_relations.png
new file mode 100644
index 000000000..1af5363e1
Binary files /dev/null and b/project_details/planning/entity_relations.png differ
diff --git a/project_details/planning/entity_relationship_diagram.md b/project_details/planning/entity_relationship_diagram.md
new file mode 100644
index 000000000..693d1d193
--- /dev/null
+++ b/project_details/planning/entity_relationship_diagram.md
@@ -0,0 +1,3 @@
+User, Post, Group, Messages
+
+
diff --git a/project_details/planning/user_stories.md b/project_details/planning/user_stories.md
new file mode 100644
index 000000000..3db24b111
--- /dev/null
+++ b/project_details/planning/user_stories.md
@@ -0,0 +1,20 @@
+# User Stories
+
+Reference the Writing User Stories final project guide in the course portal for more information about how to complete each of the sections below.
+
+## Outline User Roles
+
+Student, Peer, Group Organizer.
+
+## Draft User Stories
+
+1. As a Student, I want to register using my school email so that I can verify I am part of the Hunter College community.
+2. As a Student, I want to list my major and currently enrolled courses so that the app can find compatible hangout buddies based on academics.
+3. As a Student, I want to see profiles of potential hangout buddies so that I can quickly decide whether to connect with them using the swipe feature.
+4. As a Student, I want to filter potential buddies by their courses and preferred hangout time so that I only see people who are relevant to my current needs.
+5. As a Group Organizer, I want to create a new hangout group with a title and purpose so that I can organize a scheduled event.
+6. As a Group Organizer, I want to send invitations to other students so that they can join my scheduled hangout group.
+7. As a Peer, I want to see a list of all my active group chats and one-on-one chats so that I can easily jump into a conversation.
+8. As a Peer, I want to send and view messages in a group chat so that I can coordinate hangout plans with my group members.
+9. As a Student, I want to block another user I’ve had a negative interaction with so that they can no longer view my profile or message me.
+10. As a Peer, I want to voluntarily leave a hangout group so that it no longer appears in my active chat list.
diff --git a/project_details/planning/w1.png b/project_details/planning/w1.png
new file mode 100644
index 000000000..1846df56e
Binary files /dev/null and b/project_details/planning/w1.png differ
diff --git a/project_details/planning/w2.png b/project_details/planning/w2.png
new file mode 100644
index 000000000..3ca930739
Binary files /dev/null and b/project_details/planning/w2.png differ
diff --git a/project_details/planning/w3.png b/project_details/planning/w3.png
new file mode 100644
index 000000000..04455a7b0
Binary files /dev/null and b/project_details/planning/w3.png differ
diff --git a/planning/wireframes.md b/project_details/planning/wireframes.md
similarity index 52%
rename from planning/wireframes.md
rename to project_details/planning/wireframes.md
index fbcd15a0c..4992d2ff8 100644
--- a/planning/wireframes.md
+++ b/project_details/planning/wireframes.md
@@ -6,16 +6,15 @@ Reference the Creating an Entity Relationship Diagram final project guide in the
[👉🏾👉🏾👉🏾 List the pages you expect to have in your app, with a ⭐ next to pages you have wireframed]
-## Wireframe 1: [page title]
+## Wireframe 1: Main Section ⭐
-[👉🏾👉🏾👉🏾 include wireframe 1]
+
-## Wireframe 2: [page title]
+## Wireframe 2: Profile Section ⭐
-[👉🏾👉🏾👉🏾 include wireframe 2]
+
-## Wireframe 3: [page title]
+## Wireframe 3: Messaging Section ⭐
-[👉🏾👉🏾👉🏾 include wireframe 3]
+
-[👉🏾👉🏾👉🏾 include more wireframes as desired]
diff --git a/server/.env.example b/server/.env.example
new file mode 100644
index 000000000..4495a9a20
--- /dev/null
+++ b/server/.env.example
@@ -0,0 +1,4 @@
+# Environment Variables
+SUPABASE_URL=your_supabase_url_here
+SUPABASE_KEY=your_supabase_anon_key_here
+PORT=3000
diff --git a/server/.gitignore b/server/.gitignore
new file mode 100644
index 000000000..0a2880b39
--- /dev/null
+++ b/server/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env
diff --git a/server/config/dotenv.js b/server/config/dotenv.js
new file mode 100644
index 000000000..47fa53218
--- /dev/null
+++ b/server/config/dotenv.js
@@ -0,0 +1,9 @@
+import dotenv from "dotenv";
+
+dotenv.config();
+
+export const config = {
+ supabaseUrl: process.env.SUPABASE_URL,
+ supabaseKey: process.env.SUPABASE_KEY,
+ port: process.env.PORT || 3000,
+};
diff --git a/server/config/supabase.js b/server/config/supabase.js
new file mode 100644
index 000000000..50e16179f
--- /dev/null
+++ b/server/config/supabase.js
@@ -0,0 +1,14 @@
+import { createClient } from "@supabase/supabase-js";
+import 'dotenv/config';
+
+// Use service role key for backend to bypass RLS
+export const supabase = createClient(
+ process.env.SUPABASE_URL,
+ process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_KEY,
+ {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false
+ }
+ }
+);
\ No newline at end of file
diff --git a/server/controllers/group.js b/server/controllers/group.js
new file mode 100644
index 000000000..c61623a1e
--- /dev/null
+++ b/server/controllers/group.js
@@ -0,0 +1,75 @@
+import { supabase } from "../config/supabase.js";
+
+// Get all groups
+export const getGroups = async (req, res) => {
+ try {
+ const { data, error } = await supabase.from("group").select("*");
+
+ if (error) throw error;
+ res.json(data);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Get group by ID
+export const getGroupById = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { data, error } = await supabase
+ .from("group")
+ .select("*")
+ .eq("id", id)
+ .single();
+
+ if (error) throw error;
+ res.json(data);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Create group
+export const createGroup = async (req, res) => {
+ try {
+ const { data, error } = await supabase
+ .from("group")
+ .insert([req.body])
+ .select();
+
+ if (error) throw error;
+ res.status(201).json(data[0]);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Update group
+export const updateGroup = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { data, error } = await supabase
+ .from("group")
+ .update(req.body)
+ .eq("id", id)
+ .select();
+
+ if (error) throw error;
+ res.json(data[0]);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Delete group
+export const deleteGroup = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { error } = await supabase.from("group").delete().eq("id", id);
+
+ if (error) throw error;
+ res.json({ message: "Group deleted successfully" });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
diff --git a/server/controllers/messages.js b/server/controllers/messages.js
new file mode 100644
index 000000000..8f63bc4f2
--- /dev/null
+++ b/server/controllers/messages.js
@@ -0,0 +1,137 @@
+import { supabase } from "../config/supabase.js";
+
+// Get all conversations for a user
+export const getUserConversations = async (req, res) => {
+ try {
+ const { userId } = req.params;
+
+ // Get conversations where user is participant
+ const { data: conversations, error } = await supabase
+ .from("conversations")
+ .select("*")
+ .or(`user_id_1.eq.${userId},user_id_2.eq.${userId}`)
+ .order("last_message_at", { ascending: false });
+
+ if (error) throw error;
+
+ // For each conversation, get the other user's info
+ const conversationsWithUsers = await Promise.all(
+ conversations.map(async (conv) => {
+ const otherUserId = conv.user_id_1 === userId ? conv.user_id_2 : conv.user_id_1;
+
+ const { data: otherUser } = await supabase
+ .from("user")
+ .select("id, username, pfp")
+ .eq("id", otherUserId)
+ .single();
+
+ return {
+ ...conv,
+ otherUser,
+ };
+ })
+ );
+
+ res.json(conversationsWithUsers);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Get or create conversation between two users
+export const getOrCreateConversation = async (req, res) => {
+ try {
+ const { user1Id, user2Id } = req.body;
+ // Sort UUIDs alphabetically to ensure consistent ordering
+ const [smallerId, largerId] = [user1Id, user2Id].sort();
+
+ // Try to find existing conversation
+ let { data: conversation, error: findError } = await supabase
+ .from("conversations")
+ .select("*")
+ .eq("user_id_1", smallerId)
+ .eq("user_id_2", largerId)
+ .single();
+
+ // If doesn't exist, create it
+ if (!conversation) {
+ const { data: newConv, error: createError } = await supabase
+ .from("conversations")
+ .insert([{ user_id_1: smallerId, user_id_2: largerId }])
+ .select()
+ .single();
+
+ if (createError) throw createError;
+ conversation = newConv;
+ }
+
+ res.json(conversation);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Get messages in a conversation
+export const getConversationMessages = async (req, res) => {
+ try {
+ const { conversationId } = req.params;
+
+ const { data, error } = await supabase
+ .from("messages")
+ .select("*")
+ .eq("conversation_id", conversationId)
+ .order("created_at", { ascending: true });
+
+ if (error) throw error;
+ res.json(data);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Send a message
+export const sendMessage = async (req, res) => {
+ try {
+ const { conversation_id, sender_id, content } = req.body;
+
+ // Create the message
+ const { data: message, error: messageError } = await supabase
+ .from("messages")
+ .insert([{ conversation_id, sender_id, content }])
+ .select()
+ .single();
+
+ if (messageError) throw messageError;
+
+ // Update conversation's last_message_at
+ const { error: updateError } = await supabase
+ .from("conversations")
+ .update({ last_message_at: new Date().toISOString() })
+ .eq("id", conversation_id);
+
+ if (updateError) throw updateError;
+
+ res.status(201).json(message);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Mark messages as read
+export const markAsRead = async (req, res) => {
+ try {
+ const { conversationId, userId } = req.body;
+
+ const { error } = await supabase
+ .from("messages")
+ .update({ read: true })
+ .eq("conversation_id", conversationId)
+ .neq("sender_id", userId)
+ .eq("read", false);
+
+ if (error) throw error;
+ res.json({ message: "Messages marked as read" });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
diff --git a/server/controllers/post.js b/server/controllers/post.js
new file mode 100644
index 000000000..ddee02519
--- /dev/null
+++ b/server/controllers/post.js
@@ -0,0 +1,123 @@
+import { supabase } from "../config/supabase.js";
+
+// Get all posts
+export const getPosts = async (req, res) => {
+ try {
+ console.log("=== FETCHING POSTS - VERSION 2.0 ===");
+
+ // Get all posts
+ const { data: posts, error: postsError } = await supabase
+ .from("post")
+ .select("*")
+ .order("created_at", { ascending: false });
+
+ if (postsError) throw postsError;
+
+ console.log("Posts fetched:", posts);
+
+ // For each post, fetch the user - EXACTLY like user lookup does
+ const postsWithUsers = [];
+ for (const post of posts) {
+ console.log(`\n--- Processing post ${post.id} ---`);
+ console.log(`Post user_id: "${post.user_id}" (type: ${typeof post.user_id})`);
+
+ if (post.user_id && post.user_id !== 'anonymous') {
+ console.log(`Fetching user with id: "${post.user_id}"`);
+
+ // Fetch user EXACTLY like getUserById does
+ const { data: user, error: userError } = await supabase
+ .from("user")
+ .select("*")
+ .eq("id", post.user_id)
+ .single();
+
+ console.log("User fetch result:", user);
+ console.log("User fetch error:", userError);
+
+ postsWithUsers.push({
+ ...post,
+ user: user
+ });
+ } else {
+ console.log("Skipping user fetch (anonymous or no user_id)");
+ postsWithUsers.push(post);
+ }
+ }
+
+ console.log("\n=== FINAL RESULT ===");
+ console.log(JSON.stringify(postsWithUsers, null, 2));
+
+ res.json(postsWithUsers);
+ } catch (error) {
+ console.error("Error fetching posts:", error);
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Get post by ID
+export const getPostById = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { data, error } = await supabase
+ .from("post")
+ .select(`
+ *,
+ user:user!user_id (
+ id,
+ username
+ )
+ `)
+ .eq("id", id)
+ .single();
+
+ if (error) throw error;
+ res.json(data);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Create post
+export const createPost = async (req, res) => {
+ try {
+ const { data, error } = await supabase
+ .from("post")
+ .insert([req.body])
+ .select();
+
+ if (error) throw error;
+ res.status(201).json(data[0]);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Update post
+export const updatePost = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { data, error } = await supabase
+ .from("post")
+ .update(req.body)
+ .eq("id", id)
+ .select();
+
+ if (error) throw error;
+ res.json(data[0]);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Delete post
+export const deletePost = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { error } = await supabase.from("post").delete().eq("id", id);
+
+ if (error) throw error;
+ res.json({ message: "Post deleted successfully" });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
diff --git a/server/controllers/user.js b/server/controllers/user.js
new file mode 100644
index 000000000..aa3213a58
--- /dev/null
+++ b/server/controllers/user.js
@@ -0,0 +1,205 @@
+import { supabase } from "../config/supabase.js";
+
+// Get all users
+export const getUsers = async (req, res) => {
+ try {
+ const { data, error } = await supabase.from("user").select("*");
+
+ if (error) throw error;
+ res.json(data);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Get user by ID
+export const getUserById = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { data, error } = await supabase
+ .from("user")
+ .select("*")
+ .eq("id", id)
+ .single();
+
+ if (error) throw error;
+ res.json(data);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Get user by username
+export const getUserByUsername = async (req, res) => {
+ try {
+ const { username } = req.params;
+ const { data, error } = await supabase
+ .from("user")
+ .select("*")
+ .eq("username", username)
+ .single();
+
+ if (error) throw error;
+ res.json(data);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Create user
+export const createUser = async (req, res) => {
+ try {
+ const { data, error } = await supabase
+ .from("user")
+ .insert([req.body])
+ .select();
+
+ if (error) throw error;
+ res.status(201).json(data[0]);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Update user
+export const updateUser = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { data, error } = await supabase
+ .from("user")
+ .update(req.body)
+ .eq("id", id)
+ .select();
+
+ if (error) throw error;
+ res.json(data[0]);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Delete user
+export const deleteUser = async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { error } = await supabase.from("user").delete().eq("id", id);
+
+ if (error) throw error;
+ res.json({ message: "User deleted successfully" });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Add friend
+export const addFriend = async (req, res) => {
+ try {
+ const { userId, friendId } = req.body;
+
+ console.log('=== ADD FRIEND DEBUG ===');
+ console.log('User ID:', userId);
+ console.log('Friend ID:', friendId);
+
+ // Get current user
+ const { data: user, error: userError } = await supabase
+ .from("user")
+ .select("follows_ids")
+ .eq("id", userId)
+ .single();
+
+ if (userError) throw userError;
+
+ console.log('Current follows_ids:', user.follows_ids);
+
+ // Update follows_ids (add the friend)
+ const currentFollows = user.follows_ids || {};
+ currentFollows[friendId] = true;
+
+ console.log('Updated follows_ids:', currentFollows);
+
+ const { data, error } = await supabase
+ .from("user")
+ .update({ follows_ids: currentFollows })
+ .eq("id", userId)
+ .select();
+
+ if (error) {
+ console.error('Update error:', error);
+ throw error;
+ }
+
+ console.log('Database response:', data);
+ console.log('First item:', data?.[0]);
+ console.log('=== END ADD FRIEND DEBUG ===');
+
+ res.json(data?.[0] || { success: true });
+ } catch (error) {
+ console.error('Error in addFriend:', error);
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Remove friend
+export const removeFriend = async (req, res) => {
+ try {
+ const { userId, friendId } = req.body;
+
+ // Get current user
+ const { data: user, error: userError } = await supabase
+ .from("user")
+ .select("follows_ids")
+ .eq("id", userId)
+ .single();
+
+ if (userError) throw userError;
+
+ // Update follows_ids (remove the friend)
+ const currentFollows = user.follows_ids || {};
+ delete currentFollows[friendId];
+
+ const { data, error } = await supabase
+ .from("user")
+ .update({ follows_ids: currentFollows })
+ .eq("id", userId)
+ .select();
+
+ if (error) throw error;
+ res.json(data[0]);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
+
+// Get user's friends
+export const getFriends = async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ // Get user's follows_ids
+ const { data: user, error: userError } = await supabase
+ .from("user")
+ .select("follows_ids")
+ .eq("id", id)
+ .single();
+
+ if (userError) throw userError;
+
+ const followsIds = user.follows_ids || {};
+ const friendIds = Object.keys(followsIds).filter(fId => followsIds[fId]);
+
+ if (friendIds.length === 0) {
+ return res.json([]);
+ }
+
+ // Fetch all friends' data
+ const { data: friends, error } = await supabase
+ .from("user")
+ .select("id, username, pfp, interests")
+ .in("id", friendIds);
+
+ if (error) throw error;
+ res.json(friends);
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+};
diff --git a/server/index.html b/server/index.html
new file mode 100644
index 000000000..9be989db8
--- /dev/null
+++ b/server/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ server
+
+
+
+
+
+
diff --git a/server/package-lock.json b/server/package-lock.json
new file mode 100644
index 000000000..889f570bf
--- /dev/null
+++ b/server/package-lock.json
@@ -0,0 +1,1005 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "server",
+ "version": "1.0.0",
+ "dependencies": {
+ "@supabase/supabase-js": "^2.39.0",
+ "cors": "^2.8.5",
+ "dotenv": "^16.3.1",
+ "express": "^4.21.2"
+ }
+ },
+ "node_modules/@supabase/auth-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.81.1.tgz",
+ "integrity": "sha512-K20GgiSm9XeRLypxYHa5UCnybWc2K0ok0HLbqCej/wRxDpJxToXNOwKt0l7nO8xI1CyQ+GrNfU6bcRzvdbeopQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.81.1.tgz",
+ "integrity": "sha512-sYgSO3mlgL0NvBFS3oRfCK4OgKGQwuOWJLzfPyWg0k8MSxSFSDeN/JtrDJD5GQrxskP6c58+vUzruBJQY78AqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.81.1.tgz",
+ "integrity": "sha512-DePpUTAPXJyBurQ4IH2e42DWoA+/Qmr5mbgY4B6ZcxVc/ZUKfTVK31BYIFBATMApWraFc8Q/Sg+yxtfJ3E0wSg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.81.1.tgz",
+ "integrity": "sha512-ViQ+Kxm8BuUP/TcYmH9tViqYKGSD1LBjdqx2p5J+47RES6c+0QHedM0PPAjthMdAHWyb2LGATE9PD2++2rO/tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/phoenix": "^1.6.6",
+ "@types/ws": "^8.18.1",
+ "tslib": "2.8.1",
+ "ws": "^8.18.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.81.1.tgz",
+ "integrity": "sha512-UNmYtjnZnhouqnbEMC1D5YJot7y0rIaZx7FG2Fv8S3hhNjcGVvO+h9We/tggi273BFkiahQPS/uRsapo1cSapw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.81.1.tgz",
+ "integrity": "sha512-KSdY7xb2L0DlLmlYzIOghdw/na4gsMcqJ8u4sD6tOQJr+x3hLujU9s4R8N3ob84/1bkvpvlU5PYKa1ae+OICnw==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.81.1",
+ "@supabase/functions-js": "2.81.1",
+ "@supabase/postgrest-js": "2.81.1",
+ "@supabase/realtime-js": "2.81.1",
+ "@supabase/storage-js": "2.81.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/phoenix": {
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
+ "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 000000000..2e089d2c0
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "server",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "node --watch server.js"
+ },
+ "dependencies": {
+ "@supabase/supabase-js": "^2.39.0",
+ "cors": "^2.8.5",
+ "dotenv": "^16.3.1",
+ "express": "^4.21.2"
+ }
+}
diff --git a/server/public/vite.svg b/server/public/vite.svg
new file mode 100644
index 000000000..e7b8dfb1b
--- /dev/null
+++ b/server/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/server/routes/group.js b/server/routes/group.js
new file mode 100644
index 000000000..545b08e34
--- /dev/null
+++ b/server/routes/group.js
@@ -0,0 +1,18 @@
+import express from "express";
+import {
+ getGroups,
+ getGroupById,
+ createGroup,
+ updateGroup,
+ deleteGroup,
+} from "../controllers/group.js";
+
+const router = express.Router();
+
+router.get("/", getGroups);
+router.get("/:id", getGroupById);
+router.post("/", createGroup);
+router.put("/:id", updateGroup);
+router.delete("/:id", deleteGroup);
+
+export default router;
diff --git a/server/routes/messages.js b/server/routes/messages.js
new file mode 100644
index 000000000..4480bc479
--- /dev/null
+++ b/server/routes/messages.js
@@ -0,0 +1,18 @@
+import express from "express";
+import {
+ getUserConversations,
+ getOrCreateConversation,
+ getConversationMessages,
+ sendMessage,
+ markAsRead,
+} from "../controllers/messages.js";
+
+const router = express.Router();
+
+router.get("/conversations/:userId", getUserConversations);
+router.post("/conversations", getOrCreateConversation);
+router.get("/conversations/:conversationId/messages", getConversationMessages);
+router.post("/send", sendMessage);
+router.post("/read", markAsRead);
+
+export default router;
diff --git a/server/routes/post.js b/server/routes/post.js
new file mode 100644
index 000000000..245f6165b
--- /dev/null
+++ b/server/routes/post.js
@@ -0,0 +1,18 @@
+import express from "express";
+import {
+ getPosts,
+ getPostById,
+ createPost,
+ updatePost,
+ deletePost,
+} from "../controllers/post.js";
+
+const router = express.Router();
+
+router.get("/", getPosts);
+router.get("/:id", getPostById);
+router.post("/", createPost);
+router.put("/:id", updatePost);
+router.delete("/:id", deletePost);
+
+export default router;
diff --git a/server/routes/user.js b/server/routes/user.js
new file mode 100644
index 000000000..c0f3bcac8
--- /dev/null
+++ b/server/routes/user.js
@@ -0,0 +1,29 @@
+import express from "express";
+import {
+ getUsers,
+ getUserById,
+ getUserByUsername,
+ createUser,
+ updateUser,
+ deleteUser,
+ addFriend,
+ removeFriend,
+ getFriends,
+} from "../controllers/user.js";
+
+const router = express.Router();
+
+// More specific routes first
+router.get("/username/:username", getUserByUsername);
+router.post("/friends/add", addFriend);
+router.post("/friends/remove", removeFriend);
+router.get("/:id/friends", getFriends);
+
+// General routes
+router.get("/", getUsers);
+router.get("/:id", getUserById);
+router.post("/", createUser);
+router.put("/:id", updateUser);
+router.delete("/:id", deleteUser);
+
+export default router;
diff --git a/server/server.js b/server/server.js
new file mode 100644
index 000000000..60da84c7f
--- /dev/null
+++ b/server/server.js
@@ -0,0 +1,36 @@
+import express from "express";
+import cors from "cors";
+import { config } from "./config/dotenv.js";
+import userRoutes from "./routes/user.js";
+import postRoutes from "./routes/post.js";
+import groupRoutes from "./routes/group.js";
+import messageRoutes from "./routes/messages.js";
+
+const app = express();
+
+// Middleware
+app.use(cors());
+app.use(express.json());
+
+// Routes
+app.use("/api/users", userRoutes);
+app.use("/api/posts", postRoutes);
+app.use("/api/groups", groupRoutes);
+app.use("/api/messages", messageRoutes);
+
+// Health check
+app.get("/", (req, res) => {
+ res.json({ message: "Lexington Links API is running" });
+});
+
+// Error handling
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).json({ error: "Something went wrong!" });
+});
+
+// Start server
+const PORT = config.port;
+app.listen(PORT, () => {
+ console.log(`Server running on port ${PORT}`);
+});
diff --git a/server/tables/FINAL_USER_TABLE_MIGRATION.sql b/server/tables/FINAL_USER_TABLE_MIGRATION.sql
new file mode 100644
index 000000000..dd7cf04d0
--- /dev/null
+++ b/server/tables/FINAL_USER_TABLE_MIGRATION.sql
@@ -0,0 +1,160 @@
+-- ==============================================================================
+-- FINAL USER TABLE MIGRATION
+-- Merges 'user' and 'profiles' tables into a single 'user' table
+-- ==============================================================================
+
+-- Step 1: Drop old tables and recreate with merged schema
+-- NOTE: We must drop messages/conversations because they have foreign keys to user.id
+-- When we change user.id from integer to UUID, the foreign keys break
+-- If you need to preserve data, export tables first, then re-import with UUIDs
+
+-- Drop in order (child tables first, then parent)
+DROP TABLE IF EXISTS messages CASCADE;
+DROP TABLE IF EXISTS conversations CASCADE;
+DROP TABLE IF EXISTS "user" CASCADE;
+DROP TABLE IF EXISTS profiles CASCADE;
+
+-- Step 2: Create the unified user table
+CREATE TABLE "user" (
+ -- Core Identity (from Supabase Auth)
+ id UUID PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+
+ -- Basic Profile Info (from both tables)
+ username TEXT NOT NULL UNIQUE,
+ display_name TEXT,
+ pfp TEXT, -- profile picture URL
+ bio TEXT,
+
+ -- Location & School Info (from profiles)
+ borough TEXT,
+ year TEXT, -- Freshman, Sophomore, Junior, Senior
+
+ -- Social Data (from user table)
+ interests JSONB DEFAULT '{}', -- {"activity": true, "another": true}
+ follows_ids JSONB DEFAULT '{}', -- {"user_uuid": true}
+
+ -- Legacy fields (nullable, can be removed later if not used)
+ post_ids JSONB,
+ group_ids JSONB,
+ message_ids JSONB
+);
+
+-- Step 3: Create indexes for performance
+CREATE INDEX idx_user_username ON "user"(username);
+CREATE INDEX idx_user_borough ON "user"(borough);
+CREATE INDEX idx_user_year ON "user"(year);
+
+-- Step 4: Recreate conversations table with UUID foreign keys
+CREATE TABLE conversations (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ user_id_1 UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
+ user_id_2 UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
+ last_message_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+
+ -- Ensure user_id_1 is alphabetically before user_id_2 for UUIDs
+ CONSTRAINT unique_conversation UNIQUE (user_id_1, user_id_2),
+ CONSTRAINT check_user_order CHECK (user_id_1 < user_id_2)
+);
+
+CREATE INDEX idx_conversations_user1 ON conversations(user_id_1);
+CREATE INDEX idx_conversations_user2 ON conversations(user_id_2);
+CREATE INDEX idx_conversations_last_message ON conversations(last_message_at DESC);
+
+-- Step 5: Recreate messages table
+CREATE TABLE messages (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ conversation_id BIGINT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
+ sender_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
+ content TEXT NOT NULL,
+ read BOOLEAN DEFAULT FALSE
+);
+
+CREATE INDEX idx_messages_conversation ON messages(conversation_id);
+CREATE INDEX idx_messages_sender ON messages(sender_id);
+CREATE INDEX idx_messages_created_at ON messages(created_at DESC);
+CREATE INDEX idx_messages_unread ON messages(read) WHERE read = FALSE;
+
+-- Step 6: Enable Row Level Security (RLS) - OPTIONAL but recommended
+ALTER TABLE "user" ENABLE ROW LEVEL SECURITY;
+ALTER TABLE conversations ENABLE ROW LEVEL SECURITY;
+ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
+
+-- Users can read all profiles
+CREATE POLICY "Users can view all profiles"
+ ON "user" FOR SELECT
+ USING (true);
+
+-- Users can only update their own profile
+CREATE POLICY "Users can update own profile"
+ ON "user" FOR UPDATE
+ USING (auth.uid() = id);
+
+-- Users can insert their own profile (for signup)
+CREATE POLICY "Users can insert own profile"
+ ON "user" FOR INSERT
+ WITH CHECK (auth.uid() = id);
+
+-- Conversations: users can only see their own
+CREATE POLICY "Users can view own conversations"
+ ON conversations FOR SELECT
+ USING (auth.uid() = user_id_1 OR auth.uid() = user_id_2);
+
+CREATE POLICY "Users can create conversations"
+ ON conversations FOR INSERT
+ WITH CHECK (auth.uid() = user_id_1 OR auth.uid() = user_id_2);
+
+-- Messages: users can only see messages in their conversations
+CREATE POLICY "Users can view messages in own conversations"
+ ON messages FOR SELECT
+ USING (
+ EXISTS (
+ SELECT 1 FROM conversations
+ WHERE conversations.id = messages.conversation_id
+ AND (conversations.user_id_1 = auth.uid() OR conversations.user_id_2 = auth.uid())
+ )
+ );
+
+CREATE POLICY "Users can send messages in own conversations"
+ ON messages FOR INSERT
+ WITH CHECK (
+ auth.uid() = sender_id AND
+ EXISTS (
+ SELECT 1 FROM conversations
+ WHERE conversations.id = messages.conversation_id
+ AND (conversations.user_id_1 = auth.uid() OR conversations.user_id_2 = auth.uid())
+ )
+ );
+
+CREATE POLICY "Users can update own messages"
+ ON messages FOR UPDATE
+ USING (
+ EXISTS (
+ SELECT 1 FROM conversations
+ WHERE conversations.id = messages.conversation_id
+ AND (conversations.user_id_1 = auth.uid() OR conversations.user_id_2 = auth.uid())
+ )
+ );
+
+-- Step 7: Add helpful comments
+COMMENT ON TABLE "user" IS 'Unified user profile table - links to Supabase Auth via UUID';
+COMMENT ON COLUMN "user".id IS 'UUID from Supabase Auth (auth.users.id)';
+COMMENT ON COLUMN "user".interests IS 'JSON object: {"activity_name": true}';
+COMMENT ON COLUMN "user".follows_ids IS 'JSON object: {"friend_uuid": true}';
+COMMENT ON TABLE conversations IS 'One-on-one conversations between users';
+COMMENT ON TABLE messages IS 'Individual messages within conversations';
+
+-- Step 8: Create a function to auto-update updated_at timestamp
+CREATE OR REPLACE FUNCTION update_updated_at_column()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+CREATE TRIGGER update_user_updated_at BEFORE UPDATE ON "user"
+FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
diff --git a/server/tables/Supabase_Tables.txt b/server/tables/Supabase_Tables.txt
new file mode 100644
index 000000000..c5e8b4c97
--- /dev/null
+++ b/server/tables/Supabase_Tables.txt
@@ -0,0 +1,67 @@
+LEXINGTON LINKS DATABASE SCHEMA
+================================================================================
+
+TABLE: user (UNIFIED - merged user + profiles)
+--------------------------------------------------------------------------------
+Column Name Type
+--------------------------------------------------------------------------------
+id uuid (from Supabase Auth)
+created_at timestamp
+updated_at timestamp
+username string (UNIQUE)
+display_name string
+pfp string (profile picture URL)
+bio string
+borough string (Bronx, Brooklyn, Manhattan, Queens, Staten Island)
+year string (Freshman, Sophomore, Junior, Senior)
+interests jsonb/object {"activity": true}
+follows_ids jsonb/object {"user_uuid": true}
+post_ids nullable (legacy)
+group_ids nullable (legacy)
+message_ids nullable (legacy)
+
+TABLE: post
+--------------------------------------------------------------------------------
+Column Name Type
+--------------------------------------------------------------------------------
+id integer
+created_at timestamp
+user_id string
+image_url nullable
+caption string
+content string
+user_ids nullable
+group_limit nullable
+
+TABLE: group
+--------------------------------------------------------------------------------
+Column Name Type
+--------------------------------------------------------------------------------
+id integer
+created_at timestamp
+name string
+common_interests jsonb/object
+messages_id jsonb/object
+user_ids jsonb/object
+
+TABLE: messages
+--------------------------------------------------------------------------------
+Column Name Type
+--------------------------------------------------------------------------------
+id integer
+created_at timestamp
+conversation_id integer
+sender_id uuid
+content string
+read boolean
+
+TABLE: conversations
+--------------------------------------------------------------------------------
+Column Name Type
+--------------------------------------------------------------------------------
+id integer
+created_at timestamp
+user_id_1 uuid
+user_id_2 uuid
+last_message_at timestamp
+
diff --git a/server/tables/create_conversations_table.sql b/server/tables/create_conversations_table.sql
new file mode 100644
index 000000000..c06f8480d
--- /dev/null
+++ b/server/tables/create_conversations_table.sql
@@ -0,0 +1,24 @@
+-- Create conversations table for direct messaging
+-- This table tracks one-on-one conversations between users
+
+CREATE TABLE IF NOT EXISTS conversations (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ user_id_1 BIGINT NOT NULL,
+ user_id_2 BIGINT NOT NULL,
+ last_message_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+
+ -- Ensure user_id_1 is always less than user_id_2 to prevent duplicates
+ CONSTRAINT user_order_check CHECK (user_id_1 < user_id_2),
+
+ -- Ensure unique conversation between two users
+ CONSTRAINT unique_conversation UNIQUE (user_id_1, user_id_2)
+);
+
+-- Create index for faster lookups
+CREATE INDEX IF NOT EXISTS idx_conversations_user1 ON conversations(user_id_1);
+CREATE INDEX IF NOT EXISTS idx_conversations_user2 ON conversations(user_id_2);
+CREATE INDEX IF NOT EXISTS idx_conversations_last_message ON conversations(last_message_at DESC);
+
+-- Add comment
+COMMENT ON TABLE conversations IS 'Stores one-on-one conversations between users';
diff --git a/server/tables/create_messages_new_table.sql b/server/tables/create_messages_new_table.sql
new file mode 100644
index 000000000..562dff707
--- /dev/null
+++ b/server/tables/create_messages_new_table.sql
@@ -0,0 +1,28 @@
+-- Rename old messages table and create new one
+-- First, rename the old table (or drop it if you don't need the data)
+DROP TABLE IF EXISTS messages;
+
+-- Create new messages table for individual messages
+CREATE TABLE IF NOT EXISTS messages (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ conversation_id BIGINT NOT NULL,
+ sender_id BIGINT NOT NULL,
+ content TEXT NOT NULL,
+ read BOOLEAN DEFAULT FALSE,
+
+ -- Foreign key to conversations table
+ CONSTRAINT fk_conversation
+ FOREIGN KEY (conversation_id)
+ REFERENCES conversations(id)
+ ON DELETE CASCADE
+);
+
+-- Create indexes for faster queries
+CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
+CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id);
+CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_messages_unread ON messages(read) WHERE read = FALSE;
+
+-- Add comment
+COMMENT ON TABLE messages IS 'Stores individual messages within conversations';
diff --git a/server/tables/group.json b/server/tables/group.json
new file mode 100644
index 000000000..e5ae06d81
--- /dev/null
+++ b/server/tables/group.json
@@ -0,0 +1,7 @@
+{
+ "id": "",
+ "name": "",
+ "common_interests": {},
+ "messages_id": "",
+ "member_ids": {}
+}
diff --git a/server/tables/messages.json b/server/tables/messages.json
new file mode 100644
index 000000000..b6724e987
--- /dev/null
+++ b/server/tables/messages.json
@@ -0,0 +1,5 @@
+{
+ "id": "",
+ "messages": {},
+ "user_ids": {}
+}
diff --git a/server/tables/migrate_user_id_to_uuid.sql b/server/tables/migrate_user_id_to_uuid.sql
new file mode 100644
index 000000000..e3e376d36
--- /dev/null
+++ b/server/tables/migrate_user_id_to_uuid.sql
@@ -0,0 +1,72 @@
+-- Migration: Change user.id from integer to UUID for Supabase Auth compatibility
+-- WARNING: This will delete existing data in the user table!
+-- If you need to preserve data, export it first.
+
+-- Step 1: Drop dependent tables/constraints
+DROP TABLE IF EXISTS conversations CASCADE;
+DROP TABLE IF EXISTS messages CASCADE;
+
+-- Step 2: Drop and recreate the user table with UUID id
+DROP TABLE IF EXISTS "user" CASCADE;
+
+CREATE TABLE "user" (
+ id UUID PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ username TEXT NOT NULL,
+ pfp TEXT,
+ interests JSONB DEFAULT '{}',
+ post_ids JSONB,
+ group_ids JSONB,
+ follows_ids JSONB DEFAULT '{}',
+ message_ids JSONB
+);
+
+-- Step 3: Update post table to use UUID for user_id (it's already string, so it should work)
+-- The post.user_id is already TEXT/string, so it will accept UUIDs
+
+-- Step 4: Recreate conversations table with UUID user IDs
+CREATE TABLE conversations (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ user_id_1 UUID NOT NULL,
+ user_id_2 UUID NOT NULL,
+ last_message_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+
+ CONSTRAINT conversations_user_id_1_fkey FOREIGN KEY (user_id_1) REFERENCES "user"(id) ON DELETE CASCADE,
+ CONSTRAINT conversations_user_id_2_fkey FOREIGN KEY (user_id_2) REFERENCES "user"(id) ON DELETE CASCADE,
+ CONSTRAINT unique_conversation UNIQUE (user_id_1, user_id_2)
+);
+
+-- Step 5: Recreate messages table with UUID sender_id
+CREATE TABLE messages (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ conversation_id BIGINT NOT NULL,
+ sender_id UUID NOT NULL,
+ content TEXT NOT NULL,
+ read BOOLEAN DEFAULT FALSE,
+
+ CONSTRAINT fk_conversation
+ FOREIGN KEY (conversation_id)
+ REFERENCES conversations(id)
+ ON DELETE CASCADE,
+ CONSTRAINT messages_sender_id_fkey
+ FOREIGN KEY (sender_id)
+ REFERENCES "user"(id)
+ ON DELETE CASCADE
+);
+
+-- Step 6: Create indexes
+CREATE INDEX IF NOT EXISTS idx_conversations_user1 ON conversations(user_id_1);
+CREATE INDEX IF NOT EXISTS idx_conversations_user2 ON conversations(user_id_2);
+CREATE INDEX IF NOT EXISTS idx_conversations_last_message ON conversations(last_message_at DESC);
+
+CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
+CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id);
+CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_messages_unread ON messages(read) WHERE read = FALSE;
+
+-- Step 7: Add comments
+COMMENT ON TABLE "user" IS 'User profiles linked to Supabase Auth';
+COMMENT ON TABLE conversations IS 'One-on-one conversations between users';
+COMMENT ON TABLE messages IS 'Individual messages within conversations';
diff --git a/server/tables/post.json b/server/tables/post.json
new file mode 100644
index 000000000..ea3dbebf6
--- /dev/null
+++ b/server/tables/post.json
@@ -0,0 +1,9 @@
+{
+ "id": "",
+ "user_id": "",
+ "image_url": "",
+ "caption": "",
+ "content": "",
+ "user_ids": {},
+ "group_limit": ""
+}
diff --git a/server/tables/setup_messaging.sql b/server/tables/setup_messaging.sql
new file mode 100644
index 000000000..7a6f39423
--- /dev/null
+++ b/server/tables/setup_messaging.sql
@@ -0,0 +1,79 @@
+-- Complete messaging system setup
+-- Run this script in Supabase SQL Editor to set up the messaging tables
+
+-- Step 1: Drop old messages table if it exists
+DROP TABLE IF EXISTS messages CASCADE;
+
+-- Step 2: Create conversations table
+CREATE TABLE IF NOT EXISTS conversations (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ user_id_1 BIGINT NOT NULL,
+ user_id_2 BIGINT NOT NULL,
+ last_message_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+
+ CONSTRAINT user_order_check CHECK (user_id_1 < user_id_2),
+ CONSTRAINT unique_conversation UNIQUE (user_id_1, user_id_2)
+);
+
+-- Step 3: Create messages table
+CREATE TABLE IF NOT EXISTS messages (
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ conversation_id BIGINT NOT NULL,
+ sender_id BIGINT NOT NULL,
+ content TEXT NOT NULL,
+ read BOOLEAN DEFAULT FALSE,
+
+ CONSTRAINT fk_conversation
+ FOREIGN KEY (conversation_id)
+ REFERENCES conversations(id)
+ ON DELETE CASCADE
+);
+
+-- Step 4: Create indexes
+CREATE INDEX IF NOT EXISTS idx_conversations_user1 ON conversations(user_id_1);
+CREATE INDEX IF NOT EXISTS idx_conversations_user2 ON conversations(user_id_2);
+CREATE INDEX IF NOT EXISTS idx_conversations_last_message ON conversations(last_message_at DESC);
+
+CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
+CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id);
+CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_messages_unread ON messages(read) WHERE read = FALSE;
+
+-- -- Step 5: Enable Row Level Security (optional, recommended)
+-- ALTER TABLE conversations ENABLE ROW LEVEL SECURITY;
+-- ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
+
+-- -- Step 6: Create policies for RLS (users can only see their own conversations/messages)
+-- CREATE POLICY "Users can view their own conversations"
+-- ON conversations FOR SELECT
+-- USING (auth.uid()::bigint = user_id_1 OR auth.uid()::bigint = user_id_2);
+
+-- CREATE POLICY "Users can create conversations"
+-- ON conversations FOR INSERT
+-- WITH CHECK (auth.uid()::bigint = user_id_1 OR auth.uid()::bigint = user_id_2);
+
+-- CREATE POLICY "Users can view messages in their conversations"
+-- ON messages FOR SELECT
+-- USING (
+-- EXISTS (
+-- SELECT 1 FROM conversations
+-- WHERE conversations.id = messages.conversation_id
+-- AND (conversations.user_id_1 = auth.uid()::bigint OR conversations.user_id_2 = auth.uid()::bigint)
+-- )
+-- );
+
+-- CREATE POLICY "Users can send messages in their conversations"
+-- ON messages FOR INSERT
+-- WITH CHECK (
+-- EXISTS (
+-- SELECT 1 FROM conversations
+-- WHERE conversations.id = messages.conversation_id
+-- AND (conversations.user_id_1 = auth.uid()::bigint OR conversations.user_id_2 = auth.uid()::bigint)
+-- )
+-- );
+
+-- Comments
+COMMENT ON TABLE conversations IS 'Stores one-on-one conversations between users';
+COMMENT ON TABLE messages IS 'Stores individual messages within conversations';
diff --git a/server/tables/user.json b/server/tables/user.json
new file mode 100644
index 000000000..8d9662ebf
--- /dev/null
+++ b/server/tables/user.json
@@ -0,0 +1,10 @@
+{
+ "id": "",
+ "pfp": "",
+ "username": "",
+ "interests": {},
+ "post_ids": {},
+ "group_ids": {},
+ "follow_ids": {},
+ "message_ids": {}
+}
\ No newline at end of file
diff --git a/test-api.js b/test-api.js
new file mode 100644
index 000000000..9404d0ef4
--- /dev/null
+++ b/test-api.js
@@ -0,0 +1,87 @@
+// Test script to verify backend API is working
+// Run with: node test-api.js
+
+const testAPI = async () => {
+ const BASE_URL = "http://localhost:3000/api";
+
+ console.log("🧪 Testing Lexington Links API...\n");
+
+ try {
+ // Test 1: Health check
+ console.log("1️⃣ Testing health endpoint...");
+ const healthResponse = await fetch("http://localhost:3000");
+ const healthData = await healthResponse.json();
+ console.log("✅ Health check:", healthData);
+ console.log("");
+
+ // Test 2: Get all posts
+ console.log("2️⃣ Testing GET /api/posts...");
+ const getResponse = await fetch(`${BASE_URL}/posts`);
+ const posts = await getResponse.json();
+ console.log(`✅ Found ${posts.length} posts`);
+ console.log("Posts data:", JSON.stringify(posts, null, 2));
+ console.log("");
+
+ // Test 3: Create a post
+ console.log("3️⃣ Testing POST /api/posts...");
+ const newPost = {
+ user_id: "test-user",
+ caption: "Test Post from API Script",
+ content: "This is a test post created via the API test script",
+ photo_url: "https://via.placeholder.com/400",
+ };
+ const createResponse = await fetch(`${BASE_URL}/posts`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(newPost),
+ });
+ const createdPost = await createResponse.json();
+ console.log("✅ Created post:", createdPost);
+ console.log("");
+
+ // Test 4: Get the created post
+ if (createdPost.id) {
+ console.log("4️⃣ Testing GET /api/posts/:id...");
+ const getOneResponse = await fetch(`${BASE_URL}/posts/${createdPost.id}`);
+ const fetchedPost = await getOneResponse.json();
+ console.log("✅ Fetched post:", fetchedPost);
+ console.log("");
+
+ // Test 5: Update the post
+ console.log("5️⃣ Testing PUT /api/posts/:id...");
+ const updatedData = {
+ ...newPost,
+ caption: "UPDATED: Test Post",
+ content: "This post has been updated!",
+ };
+ const updateResponse = await fetch(`${BASE_URL}/posts/${createdPost.id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(updatedData),
+ });
+ const updatedPost = await updateResponse.json();
+ console.log("✅ Updated post:", updatedPost);
+ console.log("");
+
+ // Test 6: Delete the post
+ console.log("6️⃣ Testing DELETE /api/posts/:id...");
+ const deleteResponse = await fetch(`${BASE_URL}/posts/${createdPost.id}`, {
+ method: "DELETE",
+ });
+ const deleteResult = await deleteResponse.json();
+ console.log("✅ Deleted post:", deleteResult);
+ console.log("");
+ }
+
+ console.log("🎉 All tests passed!");
+ } catch (error) {
+ console.error("❌ Test failed:", error.message);
+ console.log("\n💡 Make sure:");
+ console.log(" - Backend server is running (npm run dev in server folder)");
+ console.log(" - Supabase credentials are configured in server/.env");
+ console.log(" - Post table exists in Supabase");
+ }
+};
+
+// Run tests
+testAPI();