Documentation
This commit is contained in:
619
backend/SCHEMA.md
Normal file
619
backend/SCHEMA.md
Normal file
@@ -0,0 +1,619 @@
|
||||
# Looking - Database Schema
|
||||
|
||||
> **MongoDB collection schemas and data models for the Looking API**
|
||||
|
||||
This document details all MongoDB collections, their schemas, relationships, indexes, and data processing hooks.
|
||||
|
||||
**Database Name**: `urge`
|
||||
**MongoDB Version**: 4.4
|
||||
**ODM**: Mongoose 4.7.4
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Collections Overview](#collections-overview)
|
||||
- [User Collection](#user-collection)
|
||||
- [Profile Collection](#profile-collection)
|
||||
- [Message Schema](#message-schema)
|
||||
- [Reset Collection](#reset-collection)
|
||||
- [Geocache Collection](#geocache-collection)
|
||||
- [Entity Relationships](#entity-relationships)
|
||||
- [Indexes](#indexes)
|
||||
- [Image Processing Hooks](#image-processing-hooks)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Collections Overview
|
||||
|
||||
| Collection | Purpose | Model File |
|
||||
| ------------- | -------------------------------- | -------------------------------------------- |
|
||||
| **users** | User accounts and authentication | [models/user.js](src/models/user.js) |
|
||||
| **profiles** | Dating profile data and stories | [models/profile.js](src/models/profile.js) |
|
||||
| **messages** | (Embedded in profiles) | [models/message.js](src/models/message.js) |
|
||||
| **resets** | Password reset tokens | [models/reset.js](src/models/reset.js) |
|
||||
| **geocaches** | Location geocoding cache | [models/geocache.js](src/models/geocache.js) |
|
||||
|
||||
---
|
||||
|
||||
## 👤 User Collection
|
||||
|
||||
**Collection Name**: `users`
|
||||
**Purpose**: User authentication, authorization, and account management
|
||||
|
||||
### Schema
|
||||
|
||||
| Field | Type | Required | Unique | Default | Description |
|
||||
| -------------- | -------- | -------- | ------ | ------------ | ------------------------------------------- |
|
||||
| **username** | String | ✅ | ✅ | - | Unique username for login |
|
||||
| **password** | String | ❌ | ❌ | - | PBKDF2 hashed password (hash + salt stored) |
|
||||
| **name.first** | String | ✅ | ❌ | - | User's first name |
|
||||
| **name.last** | String | ✅ | ❌ | - | User's last name |
|
||||
| **email** | String | ✅ | ✅ | - | Unique email address |
|
||||
| **can** | [String] | ❌ | ❌ | `['view']` | Permission array (enum values) |
|
||||
| **forceReset** | Boolean | ❌ | ❌ | `true` | Force password change on next login |
|
||||
| **updated_at** | Date | ❌ | ❌ | `Date.now()` | Last update timestamp |
|
||||
|
||||
### Permission Enum Values
|
||||
|
||||
```javascript
|
||||
["add", "edit", "delete", "manage", "super", "update", "view"];
|
||||
```
|
||||
|
||||
| Permission | Access Level |
|
||||
| ---------- | --------------------------------------- |
|
||||
| **view** | Read-only access to profiles |
|
||||
| **add** | Create new profiles |
|
||||
| **edit** | Modify existing profiles |
|
||||
| **update** | Update profile details |
|
||||
| **delete** | Remove profiles |
|
||||
| **manage** | Approve submissions, content management |
|
||||
| **super** | Full admin access, user management |
|
||||
|
||||
### Password Storage
|
||||
|
||||
Passwords are **never stored in plaintext**. The system uses:
|
||||
|
||||
**Algorithm**: PBKDF2
|
||||
**Hash**: SHA-512
|
||||
**Iterations**: 233,335
|
||||
**Hash Length**: 32 bytes
|
||||
**Salt Length**: 24 bytes
|
||||
|
||||
**Stored Format**:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"password": "hash:salt" // 32-byte hash + 24-byte salt (hex encoded)
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-Save Hooks
|
||||
|
||||
**Password Hashing** (`findOneAndUpdate` hook):
|
||||
|
||||
- Detects password changes in updates
|
||||
- Validates `password` matches `confirmPassword`
|
||||
- If `currentPassword` provided, validates before allowing change
|
||||
- Hashes new password with `authentication.hashPassword()`
|
||||
- Sets `forceReset: false` after successful password change
|
||||
|
||||
### Example Document
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": ObjectId("507f1f77bcf86cd799439011"),
|
||||
"username": "admin",
|
||||
"password": "a3f4b2c1...e5d6c7b8:1a2b3c4d...5e6f7a8b",
|
||||
"name": {
|
||||
"first": "John",
|
||||
"last": "Doe"
|
||||
},
|
||||
"email": "admin@example.com",
|
||||
"can": ["add", "edit", "delete", "manage", "super", "update", "view"],
|
||||
"forceReset": false,
|
||||
"updated_at": ISODate("2024-01-15T10:30:00Z")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Profile Collection
|
||||
|
||||
**Collection Name**: `profiles`
|
||||
**Purpose**: User profile data, stories, and message threads
|
||||
|
||||
### Schema
|
||||
|
||||
| Field | Type | Required | Index | Default | Description |
|
||||
| ------------- | --------- | -------- | ----- | ------- | ------------------------------- |
|
||||
| **order** | Number | ❌ | ❌ | `0` | Display order for sorting |
|
||||
| **details** | Object | ❌ | ❌ | `{}` | Profile information (see below) |
|
||||
| **messages** | [Message] | ❌ | ❌ | `[]` | Array of embedded messages |
|
||||
| **submitted** | Boolean | ❌ | ❌ | `false` | Story submitted by user |
|
||||
| **approved** | Boolean | ❌ | ❌ | `false` | Approved by admin |
|
||||
|
||||
### Details Object Schema
|
||||
|
||||
| Field | Type | Index | Default | Description |
|
||||
| ---------------------- | -------- | ----- | --------------------------------- | ----------------------------------------------------- |
|
||||
| **details.about** | String | ❌ | - | User's story/bio text |
|
||||
| **details.age** | Number | ✅ | `0` | User's age |
|
||||
| **details.location** | String | ❌ | - | City, State format |
|
||||
| **details.name** | String | ✅ | - | Display name |
|
||||
| **details.pic.detail** | String | ❌ | `"profile/default_detail.png"` | Full-size profile photo path |
|
||||
| **details.pic.thumb** | String | ❌ | `"profile/default_thumbnail.png"` | Thumbnail photo path |
|
||||
| **details.position** | [String] | ❌ | - | Position preferences (Top/Bottom/Versatile) |
|
||||
| **details.looking** | [String] | ❌ | - | What user is looking for (Dates/Friends/Relationship) |
|
||||
| **details.tribes** | [String] | ❌ | - | Tribes/groups (Geek/Jock/Bear/Otter/etc.) |
|
||||
| **details.ethnos** | [String] | ❌ | - | Ethnicity (White/Black/Latino/Asian/etc.) |
|
||||
|
||||
### Pre-Save Hooks
|
||||
|
||||
**Image Processing** (both `save` and `findOneAndUpdate` hooks):
|
||||
|
||||
1. **Detects Base64 Image Data**:
|
||||
- Checks if `details.pic.detail` or `details.pic.thumb` is an object/base64 string
|
||||
2. **Processes Images**:
|
||||
|
||||
- **Detail Image**: Calls `Images.saveProfileDetailImage()`
|
||||
|
||||
- Decodes base64
|
||||
- Generates unique filename
|
||||
- Saves to `src/images/profile/<filename>`
|
||||
- Returns path string
|
||||
|
||||
- **Thumbnail**: Calls `Images.saveProfileThumbnailImage()`
|
||||
- Same process for thumbnails
|
||||
- May resize/compress image
|
||||
|
||||
3. **Updates Document**:
|
||||
- Replaces base64 data with file path string
|
||||
- Continues with save operation
|
||||
|
||||
### Example Document
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": ObjectId("507f1f77bcf86cd799439011"),
|
||||
"order": 1,
|
||||
"details": {
|
||||
"name": "John",
|
||||
"age": 28,
|
||||
"location": "San Francisco, CA",
|
||||
"about": "I've been using dating apps for 5 years. My experience has been...",
|
||||
"pic": {
|
||||
"thumb": "profile/john_1234567890_thumbnail.png",
|
||||
"detail": "profile/john_1234567890_detail.png"
|
||||
},
|
||||
"position": ["Top", "Versatile"],
|
||||
"looking": ["Dates", "Friends"],
|
||||
"tribes": ["Geek", "Jock"],
|
||||
"ethnos": ["White", "Latino"]
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"_id": ObjectId("507f191e810c19729de860ea"),
|
||||
"text": "What brought you to dating apps?",
|
||||
"isUser": false,
|
||||
"timestamp": ISODate("2024-01-15T10:30:00Z")
|
||||
},
|
||||
{
|
||||
"_id": ObjectId("507f191e810c19729de860eb"),
|
||||
"text": "I moved to San Francisco for work and didn't know anyone...",
|
||||
"image": "message/john_response_1234567890.png",
|
||||
"isUser": true,
|
||||
"timestamp": ISODate("2024-01-15T10:32:00Z")
|
||||
}
|
||||
],
|
||||
"submitted": true,
|
||||
"approved": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💬 Message Schema
|
||||
|
||||
**Collection**: Embedded in `profiles.messages` array
|
||||
**Purpose**: Q&A conversation threads between interviewer and profile subject
|
||||
|
||||
### Schema
|
||||
|
||||
| Field | Type | Required | Index | Default | Description |
|
||||
| ------------- | ------- | -------- | ----- | ------------ | ------------------------------------------ |
|
||||
| **text** | String | ❌ | ❌ | - | Message content |
|
||||
| **image** | String | ❌ | ✅ | - | Image file path (if message has photo) |
|
||||
| **isUser** | Boolean | ✅ | ✅ | `false` | `true` = user response, `false` = question |
|
||||
| **timestamp** | Date | ❌ | ❌ | `Date.now()` | Message timestamp |
|
||||
|
||||
### Message Types
|
||||
|
||||
| `isUser` | Type | Purpose | Display |
|
||||
| --------- | -------- | ---------------------- | --------------------- |
|
||||
| **false** | Question | Interviewer's question | Left-aligned, bold |
|
||||
| **true** | Response | User's answer | Right-aligned, normal |
|
||||
|
||||
### Pre-Save Hooks
|
||||
|
||||
**Image Processing** (both `save` and `findOneAndUpdate` hooks):
|
||||
|
||||
1. **Detects Image Data**:
|
||||
- Checks if `message.image` is an object (base64)
|
||||
2. **Processes Image**:
|
||||
- Calls `Images.saveMessageImage()`
|
||||
- Decodes base64 data
|
||||
- Generates unique filename
|
||||
- Saves to `src/images/message/<filename>`
|
||||
- Returns path string
|
||||
3. **Updates Document**:
|
||||
- Replaces base64 with file path
|
||||
- Continues with save
|
||||
|
||||
### Example Messages
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"_id": ObjectId("507f191e810c19729de860ea"),
|
||||
"text": "What's your most memorable dating app experience?",
|
||||
"isUser": false,
|
||||
"timestamp": ISODate("2024-01-15T10:30:00Z")
|
||||
},
|
||||
{
|
||||
"_id": ObjectId("507f191e810c19729de860eb"),
|
||||
"text": "I matched with someone who shared my love of hiking...",
|
||||
"image": "message/hiking_photo_1234567890.png",
|
||||
"isUser": true,
|
||||
"timestamp": ISODate("2024-01-15T10:35:00Z")
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Reset Collection
|
||||
|
||||
**Collection Name**: `resets`
|
||||
**Purpose**: Password reset token management
|
||||
|
||||
### Schema
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
| -------------- | -------- | -------- | ------------ | -------------------------- |
|
||||
| **user** | ObjectId | ✅ | - | Reference to `users._id` |
|
||||
| **expires** | Date | ❌ | `Date.now()` | Token expiration timestamp |
|
||||
| **used** | Boolean | ❌ | `false` | Token already consumed |
|
||||
| **updated_at** | Date | ❌ | `Date.now()` | Last update timestamp |
|
||||
|
||||
### Token Generation
|
||||
|
||||
**HMAC-SHA1 Token**:
|
||||
|
||||
```javascript
|
||||
const secret = "Creepily hooking the gays up since 2008!";
|
||||
const token = crypto
|
||||
.createHmac("sha1", secret)
|
||||
.update(userId + "|" + expires)
|
||||
.digest("hex");
|
||||
```
|
||||
|
||||
**Expiration**: Typically 1-24 hours from creation
|
||||
|
||||
### Reset Workflow
|
||||
|
||||
1. **User requests reset**: `POST /auth/reset` with username
|
||||
2. **System creates reset document**: Links user, generates token, sets expiration
|
||||
3. **Email sent**: Password reset link with `/auth/reset/:id/:token`
|
||||
4. **User clicks link**: `GET /auth/reset/:id/:token` validates token
|
||||
5. **User sets new password**: `PUT /auth/reset/:id/:token` with new password
|
||||
6. **Token marked as used**: `used: true` prevents reuse
|
||||
|
||||
### Example Document
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": ObjectId("507f1f77bcf86cd799439020"),
|
||||
"user": ObjectId("507f1f77bcf86cd799439011"),
|
||||
"expires": ISODate("2024-01-16T10:00:00Z"),
|
||||
"used": false,
|
||||
"updated_at": ISODate("2024-01-15T10:00:00Z")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Geocache Collection
|
||||
|
||||
**Collection Name**: `geocaches`
|
||||
**Purpose**: Cache Google Maps API geocoding results to reduce API calls
|
||||
|
||||
### Schema
|
||||
|
||||
| Field | Type | Required | Unique | Default | Description |
|
||||
| ------------------- | -------- | -------- | ------ | --------- | ---------------------------------------------------- |
|
||||
| **key** | String | ✅ | ✅ | - | Original location string (e.g., "San Francisco, CA") |
|
||||
| **formatted** | String | ✅ | ❌ | - | Google's formatted address |
|
||||
| **loc.type** | String | ❌ | ❌ | `"Point"` | GeoJSON type |
|
||||
| **loc.coordinates** | [Number] | ❌ | ❌ | `[0, 0]` | [longitude, latitude] array |
|
||||
| **georesult** | Mixed | ❌ | ❌ | - | Full Google Maps API response |
|
||||
|
||||
### GeoJSON Structure
|
||||
|
||||
MongoDB's geospatial queries require **GeoJSON Point** format:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "Point",
|
||||
"coordinates": [-122.4194, 37.7749] // [longitude, latitude]
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **Note**: Coordinates are `[lng, lat]`, not `[lat, lng]`
|
||||
|
||||
### Example Document
|
||||
|
||||
```json
|
||||
{
|
||||
"_id": ObjectId("507f1f77bcf86cd799439030"),
|
||||
"key": "San Francisco, CA",
|
||||
"formatted": "San Francisco, California, USA",
|
||||
"loc": {
|
||||
"type": "Point",
|
||||
"coordinates": [-122.4194, 37.7749]
|
||||
},
|
||||
"georesult": {
|
||||
"address_components": [ ... ],
|
||||
"formatted_address": "San Francisco, California, USA",
|
||||
"geometry": {
|
||||
"location": {
|
||||
"lat": 37.7749,
|
||||
"lng": -122.4194
|
||||
}
|
||||
},
|
||||
"place_id": "ChIJIQBpAG2ahYAR_6128GcTUEo"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
**Geocoding Workflow**:
|
||||
|
||||
1. Check if location exists in geocache by `key`
|
||||
2. If cached, return stored result (no API call)
|
||||
3. If not cached:
|
||||
- Call Google Maps Geocoding API
|
||||
- Store result in geocache
|
||||
- Return result
|
||||
4. Future requests for same location use cache
|
||||
|
||||
**Benefits**:
|
||||
|
||||
- Reduces API costs
|
||||
- Faster response times
|
||||
- Works offline for cached locations
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Entity Relationships
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER ||--o{ RESET : "can have"
|
||||
PROFILE ||--o{ MESSAGE : "contains"
|
||||
PROFILE }o--|| GEOCACHE : "references location"
|
||||
|
||||
USER {
|
||||
ObjectId _id PK
|
||||
string username UK
|
||||
string password
|
||||
string email UK
|
||||
array can
|
||||
boolean forceReset
|
||||
}
|
||||
|
||||
PROFILE {
|
||||
ObjectId _id PK
|
||||
number order
|
||||
object details
|
||||
array messages
|
||||
boolean submitted
|
||||
boolean approved
|
||||
}
|
||||
|
||||
MESSAGE {
|
||||
ObjectId _id PK
|
||||
string text
|
||||
string image
|
||||
boolean isUser
|
||||
date timestamp
|
||||
}
|
||||
|
||||
RESET {
|
||||
ObjectId _id PK
|
||||
ObjectId user FK
|
||||
date expires
|
||||
boolean used
|
||||
}
|
||||
|
||||
GEOCACHE {
|
||||
ObjectId _id PK
|
||||
string key UK
|
||||
string formatted
|
||||
object loc
|
||||
object georesult
|
||||
}
|
||||
```
|
||||
|
||||
### Relationship Details
|
||||
|
||||
| Relationship | Type | Description |
|
||||
| ---------------------- | ---------------------- | ---------------------------------------------- |
|
||||
| **User → Reset** | One-to-Many | User can have multiple reset tokens (history) |
|
||||
| **Profile → Message** | One-to-Many (Embedded) | Profile contains array of messages |
|
||||
| **Profile → Geocache** | Many-to-One (Soft) | Profile location string may match geocache key |
|
||||
|
||||
**Note**: No foreign key constraints in MongoDB. Relationships are application-level only.
|
||||
|
||||
---
|
||||
|
||||
## 📇 Indexes
|
||||
|
||||
### Current Indexes
|
||||
|
||||
| Collection | Field | Type | Purpose |
|
||||
| ------------- | -------------- | -------- | -------------------------------------- |
|
||||
| **users** | `username` | Unique | Fast login lookups, prevent duplicates |
|
||||
| **users** | `email` | Unique | Email validation, prevent duplicates |
|
||||
| **profiles** | `details.age` | Standard | Age range filtering |
|
||||
| **profiles** | `details.name` | Standard | Name search/sorting |
|
||||
| **messages** | `image` | Standard | Find messages with images |
|
||||
| **messages** | `isUser` | Standard | Filter by message type |
|
||||
| **geocaches** | `key` | Unique | Location lookup cache |
|
||||
|
||||
### Index Usage Queries
|
||||
|
||||
```javascript
|
||||
// Fast age range query
|
||||
db.profiles
|
||||
.find({
|
||||
"details.age": { $gte: 25, $lte: 35 },
|
||||
})
|
||||
.sort({ order: 1 });
|
||||
|
||||
// User login
|
||||
db.users.findOne({ username: "admin" });
|
||||
|
||||
// Geocache lookup
|
||||
db.geocaches.findOne({ key: "San Francisco, CA" });
|
||||
|
||||
// Messages with images
|
||||
db.profiles.find({ "messages.image": { $exists: true } });
|
||||
```
|
||||
|
||||
### Recommended Additional Indexes
|
||||
|
||||
For production optimization:
|
||||
|
||||
```javascript
|
||||
// Approved profiles (most common query)
|
||||
db.profiles.createIndex({ approved: 1, order: 1 });
|
||||
|
||||
// Submitted profiles (admin review)
|
||||
db.profiles.createIndex({ submitted: 1, approved: 1 });
|
||||
|
||||
// User permissions lookup
|
||||
db.users.createIndex({ can: 1 });
|
||||
|
||||
// Location geospatial queries
|
||||
db.geocaches.createIndex({ loc: "2dsphere" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Image Processing Hooks
|
||||
|
||||
### Overview
|
||||
|
||||
Images are processed **automatically** via Mongoose pre-save hooks before documents are saved to MongoDB.
|
||||
|
||||
**Supported Formats**:
|
||||
|
||||
- Base64 encoded strings (from frontend)
|
||||
- Data URLs: `data:image/png;base64,iVBORw0KGg...`
|
||||
- File paths (strings are passed through)
|
||||
|
||||
### Processing Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Document Save Triggered] --> B{Field is Base64?}
|
||||
B -->|Yes| C[Extract Base64 Data]
|
||||
B -->|No string path| D[Skip Processing]
|
||||
C --> E[Decode to Binary]
|
||||
E --> F[Generate Unique Filename]
|
||||
F --> G{Image Type?}
|
||||
G -->|Profile Detail| H[Save to src/images/profile/]
|
||||
G -->|Profile Thumb| I[Save to src/images/profile/]
|
||||
G -->|Message Image| J[Save to src/images/message/]
|
||||
H --> K[Return File Path]
|
||||
I --> K
|
||||
J --> K
|
||||
K --> L[Update Document Field]
|
||||
L --> M[Continue Save to MongoDB]
|
||||
D --> M
|
||||
```
|
||||
|
||||
### Image Modules
|
||||
|
||||
**Location**: `modules/images.js`
|
||||
|
||||
**Functions**:
|
||||
|
||||
- `saveProfileDetailImage(data, callback)` - Processes full-size profile photos
|
||||
- `saveProfileThumbnailImage(data, callback)` - Processes profile thumbnails
|
||||
- `saveMessageImage(data, callback)` - Processes message attachments
|
||||
|
||||
**Filename Format**:
|
||||
|
||||
```
|
||||
<type>_<shortid>_<timestamp>.<ext>
|
||||
Example: profile_aB3xF2_1705315200000.png
|
||||
```
|
||||
|
||||
### Storage Locations
|
||||
|
||||
| Image Type | Directory | Example Path |
|
||||
| --------------------- | --------------------- | ------------------------------------ |
|
||||
| **Profile Detail** | `src/images/profile/` | `profile/john_detail_1234567890.png` |
|
||||
| **Profile Thumbnail** | `src/images/profile/` | `profile/john_thumb_1234567890.png` |
|
||||
| **Message Image** | `src/images/message/` | `message/response_1234567890.png` |
|
||||
| **Additional** | `src/images/cruise/` | `cruise/event_1234567890.png` |
|
||||
|
||||
### Error Handling
|
||||
|
||||
If image processing fails:
|
||||
|
||||
- **Error logged** via Winston logger
|
||||
- **Save continues** with original data
|
||||
- **Client receives error** in response
|
||||
|
||||
**Example Error**:
|
||||
|
||||
```javascript
|
||||
Logger.error(
|
||||
"[MessageSchema.pre(save)] There was an error processing the message image.",
|
||||
{
|
||||
error: err,
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Volume Persistence
|
||||
|
||||
**Development**: Images stored in DevContainer volume
|
||||
**Production**: Images stored in Docker volume `api_images`
|
||||
|
||||
**Volume Mount**:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- api_images:/app/src/images
|
||||
```
|
||||
|
||||
This ensures images persist across container restarts.
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[Backend README](README.md)** - Server setup and development
|
||||
- **[API Reference](API.md)** - Complete REST API documentation
|
||||
- **[Environment Variables](.env.example)** - Configuration
|
||||
- **[Root README](../README.md)** - Project overview
|
||||
- **[Deployment Guide](../DEPLOYMENT.md)** - Production deployment
|
||||
|
||||
---
|
||||
|
||||
**Need Help?** Check the backend [troubleshooting section](README.md#troubleshooting) for database-related issues.
|
||||
Reference in New Issue
Block a user