nothex commited on
Commit
67a6408
Β·
1 Parent(s): 5af3152

refactor: harden security, optimize deployment, and project-wide cleanup

Browse files

- Initialized app.py for deployment tool compatibility
- Secured auth_utils by removing unsafe JWT fallbacks and implementing strict secret validation
- Cleaned up redundant routes and static mounts in main.py
- Aligned Dockerfile to Python 3.11 for dependency stability (PaddleOCR fix)
- Rewrote render.yaml to include managed Redis and dedicated Celery worker services
- Sanitized .gitignore and updated .env.example with missing critical secrets

Files changed (9) hide show
  1. .gitignore +1 -2
  2. ARCHITECTURE.md +8 -6
  3. Dockerfile +13 -13
  4. README.md +13 -11
  5. app.py +4 -1
  6. backend/core/auth_utils.py +24 -20
  7. backend/core/cache_manager.py +3 -3
  8. backend/main.py +39 -23
  9. render.yaml +54 -4
.gitignore CHANGED
@@ -1,5 +1,4 @@
1
  .env
2
- D:\Work\replacements.txt
3
  __pycache__/
4
  *.pyc
5
  *.pyo
@@ -16,4 +15,4 @@ intent_feedback.jsonl
16
  note_to_me.txt
17
  *.pkl
18
 
19
- .dual-graph/
 
1
  .env
 
2
  __pycache__/
3
  *.pyc
4
  *.pyo
 
15
  note_to_me.txt
16
  *.pkl
17
 
18
+ .dual-graph/
ARCHITECTURE.md CHANGED
@@ -1,15 +1,17 @@
1
- # Nexus β€” Architecture Guide
 
 
2
 
3
  > Read this when starting a new session, onboarding someone, or after a long break.
4
  > Everything the system does, how the pieces connect, and why decisions were made the way they were.
5
 
6
  ---
7
 
8
- ## What Is Nexus?
9
 
10
- Nexus is a **multi-tenant RAG (Retrieval-Augmented Generation) platform**.
11
 
12
- Users upload PDF documents. They ask questions in natural language. Nexus finds the most relevant passages and uses an AI to generate accurate, streamed answers with source citations.
13
 
14
  **What makes it non-trivial:**
15
  - Each user sees only their own documents, enforced at the database level (RLS)
@@ -26,7 +28,7 @@ Users upload PDF documents. They ask questions in natural language. Nexus finds
26
  ## Project Structure
27
 
28
  ```
29
- nexus/
30
  β”œβ”€β”€ backend/
31
  β”‚ β”œβ”€β”€ main.py FastAPI app, CORS, rate limiter, Celery lifespan
32
  β”‚ β”œβ”€β”€ api/
@@ -363,7 +365,7 @@ After classification, the winning category's centroid is updated with this docum
363
 
364
  ## The Three Self-Improvement Loops
365
 
366
- Nexus has three feedback loops that make it more accurate over time:
367
 
368
  **Loop 1 β€” Intent classifier (every 25 queries)**
369
  User queries logged to `intent_feedback`. Every 25 new rows, the model retrains automatically and saves to disk. Learns the specific query patterns of your users.
 
1
+ # Morpheus β€” Architecture Guide
2
+
3
+ > Ask anything. Your documents answer.
4
 
5
  > Read this when starting a new session, onboarding someone, or after a long break.
6
  > Everything the system does, how the pieces connect, and why decisions were made the way they were.
7
 
8
  ---
9
 
10
+ ## What Is Morpheus?
11
 
12
+ Morpheus is a **multi-tenant RAG (Retrieval-Augmented Generation) platform**.
13
 
14
+ Users upload PDF documents. They ask questions in natural language. Morpheus finds the most relevant passages and uses an AI to generate accurate, streamed answers with source citations.
15
 
16
  **What makes it non-trivial:**
17
  - Each user sees only their own documents, enforced at the database level (RLS)
 
28
  ## Project Structure
29
 
30
  ```
31
+ morpheus/
32
  β”œβ”€β”€ backend/
33
  β”‚ β”œβ”€β”€ main.py FastAPI app, CORS, rate limiter, Celery lifespan
34
  β”‚ β”œβ”€β”€ api/
 
365
 
366
  ## The Three Self-Improvement Loops
367
 
368
+ Morpheus has three feedback loops that make it more accurate over time:
369
 
370
  **Loop 1 β€” Intent classifier (every 25 queries)**
371
  User queries logged to `intent_feedback`. Every 25 new rows, the model retrains automatically and saves to disk. Learns the specific query patterns of your users.
Dockerfile CHANGED
@@ -1,16 +1,16 @@
1
  # 1. Use an official lightweight Python image
2
- FROM python:3.10-slim
3
 
4
  # 2. Install system dependencies required for 'unstructured' PDF OCR
5
  RUN apt-get update && apt-get install -y \
6
- tesseract-ocr \
7
- poppler-utils \
8
- libmagic-dev \
9
- libgl1 \
10
- libglib2.0-0 \
11
- libgomp1 \
12
- libgthread-2.0-0 \
13
- && rm -rf /var/lib/apt/lists/*
14
 
15
  # 3. Create a non-root user (Required by Hugging Face for security)
16
  RUN useradd -m -u 1000 user
@@ -31,10 +31,10 @@ COPY --chown=user:user . .
31
  ARG PREBUILD_ML_ASSETS=1
32
  ARG NEXUS_BUILD_ASSETS_MODE=light
33
  RUN if [ "$PREBUILD_ML_ASSETS" = "1" ]; then \
34
- NEXUS_BUILD_ASSETS_MODE=$NEXUS_BUILD_ASSETS_MODE python -m backend.core.build_ml_assets ; \
35
- else \
36
- echo "Skipping ML asset pre-build"; \
37
- fi
38
 
39
  # 8. Start FastAPI (7860 is the HF standard, but Railway uses $PORT)
40
  ENV PORT=7860
 
1
  # 1. Use an official lightweight Python image
2
+ FROM python:3.11-slim
3
 
4
  # 2. Install system dependencies required for 'unstructured' PDF OCR
5
  RUN apt-get update && apt-get install -y \
6
+ tesseract-ocr \
7
+ poppler-utils \
8
+ libmagic-dev \
9
+ libgl1 \
10
+ libglib2.0-0 \
11
+ libgomp1 \
12
+ libgthread-2.0-0 \
13
+ && rm -rf /var/lib/apt/lists/*
14
 
15
  # 3. Create a non-root user (Required by Hugging Face for security)
16
  RUN useradd -m -u 1000 user
 
31
  ARG PREBUILD_ML_ASSETS=1
32
  ARG NEXUS_BUILD_ASSETS_MODE=light
33
  RUN if [ "$PREBUILD_ML_ASSETS" = "1" ]; then \
34
+ NEXUS_BUILD_ASSETS_MODE=$NEXUS_BUILD_ASSETS_MODE python -m backend.core.build_ml_assets ; \
35
+ else \
36
+ echo "Skipping ML asset pre-build"; \
37
+ fi
38
 
39
  # 8. Start FastAPI (7860 is the HF standard, but Railway uses $PORT)
40
  ENV PORT=7860
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Nexus RAG
3
  emoji: 🧠
4
  colorFrom: green
5
  colorTo: blue
@@ -8,7 +8,9 @@ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
- # Nexus β€” Multimodal RAG Engine
 
 
12
 
13
  A multi-tenant Retrieval-Augmented Generation platform. Upload PDFs, ask questions in natural language, get accurate answers with source citations β€” streamed token by token, scoped strictly to your documents.
14
 
@@ -26,7 +28,7 @@ A multi-tenant Retrieval-Augmented Generation platform. Upload PDFs, ask questio
26
  ## Project Structure
27
 
28
  ```
29
- nexus/
30
  β”œβ”€β”€ backend/
31
  β”‚ β”œβ”€β”€ main.py FastAPI entry point, rate limiter, Celery lifespan
32
  β”‚ β”œβ”€β”€ api/
@@ -87,8 +89,8 @@ nexus/
87
  ### 2. Clone and install
88
 
89
  ```bash
90
- git clone https://github.com/your-username/nexus.git
91
- cd nexus
92
  python -m venv .venv
93
  source .venv/bin/activate # Windows: .venv\Scripts\activate
94
  pip install -r requirements.txt
@@ -146,10 +148,10 @@ Create a user account via Supabase Auth (Auth β†’ Users β†’ Invite), then sign i
146
 
147
  ```bash
148
  # Downloads embedding model, trains intent classifier
149
- NEXUS_BUILD_ASSETS_MODE=full python -m backend.core.build_ml_assets
150
 
151
  # Downloads embedding model only (faster, recommended for first run)
152
- NEXUS_BUILD_ASSETS_MODE=light python -m backend.core.build_ml_assets
153
  ```
154
 
155
  ### 8. Seed the document classifier (after your first ingestion)
@@ -166,8 +168,8 @@ curl -X POST http://localhost:8000/api/v1/admin/warmup \
166
  Build and run the full stack as a single container:
167
 
168
  ```bash
169
- docker build -t nexus-rag .
170
- docker run -p 8000:7860 --env-file .env nexus-rag
171
  ```
172
 
173
  > The Dockerfile is configured for HuggingFace Spaces (port 7860, non-root user). For local use, the container maps 8000 β†’ 7860.
@@ -183,14 +185,14 @@ docker run -p 8000:7860 --env-file .env nexus-rag
183
  3. Render reads `render.yaml` automatically
184
  4. In the Render dashboard β†’ Environment tab, add all your `.env` values
185
  5. Add a Redis service: Render β†’ New β†’ Redis (free tier) β†’ copy the internal URL to `REDIS_URL`
186
- 6. Deploy β€” copy your Render URL (e.g. `https://nexus-api.onrender.com`)
187
 
188
  ### Frontend β†’ Vercel
189
 
190
  1. Update `API_URL` in `frontend/js/config.js` to your Render URL
191
  2. [vercel.com](https://vercel.com) β†’ New Project β†’ connect your repo
192
  3. Vercel reads `vercel.json` automatically
193
- 4. Deploy β€” copy your Vercel URL (e.g. `https://nexus.vercel.app`)
194
 
195
  ### After both are deployed
196
 
 
1
  ---
2
+ title: Morpheus RAG
3
  emoji: 🧠
4
  colorFrom: green
5
  colorTo: blue
 
8
  pinned: false
9
  ---
10
 
11
+ # Morpheus
12
+
13
+ > Ask anything. Your documents answer.
14
 
15
  A multi-tenant Retrieval-Augmented Generation platform. Upload PDFs, ask questions in natural language, get accurate answers with source citations β€” streamed token by token, scoped strictly to your documents.
16
 
 
28
  ## Project Structure
29
 
30
  ```
31
+ morpheus/
32
  β”œβ”€β”€ backend/
33
  β”‚ β”œβ”€β”€ main.py FastAPI entry point, rate limiter, Celery lifespan
34
  β”‚ β”œβ”€β”€ api/
 
89
  ### 2. Clone and install
90
 
91
  ```bash
92
+ git clone https://github.com/your-username/morpheus.git
93
+ cd morpheus
94
  python -m venv .venv
95
  source .venv/bin/activate # Windows: .venv\Scripts\activate
96
  pip install -r requirements.txt
 
148
 
149
  ```bash
150
  # Downloads embedding model, trains intent classifier
151
+ Morpheus_BUILD_ASSETS_MODE=full python -m backend.core.build_ml_assets
152
 
153
  # Downloads embedding model only (faster, recommended for first run)
154
+ Morpheus_BUILD_ASSETS_MODE=light python -m backend.core.build_ml_assets
155
  ```
156
 
157
  ### 8. Seed the document classifier (after your first ingestion)
 
168
  Build and run the full stack as a single container:
169
 
170
  ```bash
171
+ docker build -t morpheus-rag .
172
+ docker run -p 8000:7860 --env-file .env morpheus-rag
173
  ```
174
 
175
  > The Dockerfile is configured for HuggingFace Spaces (port 7860, non-root user). For local use, the container maps 8000 β†’ 7860.
 
185
  3. Render reads `render.yaml` automatically
186
  4. In the Render dashboard β†’ Environment tab, add all your `.env` values
187
  5. Add a Redis service: Render β†’ New β†’ Redis (free tier) β†’ copy the internal URL to `REDIS_URL`
188
+ 6. Deploy β€” copy your Render URL (e.g. `https://morpheus-api.onrender.com`)
189
 
190
  ### Frontend β†’ Vercel
191
 
192
  1. Update `API_URL` in `frontend/js/config.js` to your Render URL
193
  2. [vercel.com](https://vercel.com) β†’ New Project β†’ connect your repo
194
  3. Vercel reads `vercel.json` automatically
195
+ 4. Deploy β€” copy your Vercel URL (e.g. `https://morpheus.vercel.app`)
196
 
197
  ### After both are deployed
198
 
app.py CHANGED
@@ -1 +1,4 @@
1
- from backend.main import app
 
 
 
 
1
+ # Entry point for deployments that expect app.py at the project root.
2
+ # All app logic lives in backend/main.py β€” this just re-exports the app object
3
+ # so tools like gunicorn can target either `app:app` or `backend.main:app`.
4
+ from backend.main import app # noqa: F401
backend/core/auth_utils.py CHANGED
@@ -19,6 +19,7 @@ from fastapi import Header, HTTPException, status
19
 
20
  # ── Low-level JWT helpers ─────────────────────────────────────────────────────
21
 
 
22
  def extract_jwt_sub(access_token: str) -> str:
23
  """
24
  Extract the Supabase user id (JWT `sub`) while strictly verifying the signature.
@@ -31,28 +32,29 @@ def extract_jwt_sub(access_token: str) -> str:
31
  import jwt
32
 
33
  if not config.SUPABASE_JWT_SECRET:
34
- # Fallback to insecure decoding ONLY if secret is missing
35
- # (e.g., local testing without full env)
36
- import logging
37
- logging.getLogger("nexus.auth").warning("SUPABASE_JWT_SECRET missing! Decoding JWT unsafely.")
38
- payload = jwt.decode(access_token, options={"verify_signature": False})
39
- else:
40
- try:
41
- payload = jwt.decode(
42
- access_token,
43
- key=config.SUPABASE_JWT_SECRET,
44
- algorithms=["HS256"],
45
- audience="authenticated",
46
- )
47
- except jwt.ExpiredSignatureError:
48
- raise ValueError("JWT has expired")
49
- except jwt.InvalidTokenError as e:
50
- raise ValueError(f"Invalid JWT signature or payload: {e}")
51
 
52
  # Supabase uses `sub` for the user UUID.
53
  sub = payload.get("sub") or payload.get("user_id") or payload.get("uid")
54
  if not sub:
55
- raise ValueError("JWT does not contain a user id claim (`sub` / `uid` / `user_id`).")
 
 
56
 
57
  return str(sub)
58
 
@@ -63,12 +65,14 @@ def safe_extract_jwt_sub(access_token: Optional[str]) -> Optional[str]:
63
  return extract_jwt_sub(access_token) if access_token else None
64
  except Exception as e:
65
  import logging
66
- logging.getLogger("nexus.auth").debug("JWT extraction failed: %s", e)
 
67
  return None
68
 
69
 
70
  # ── FastAPI dependency ────────────────────────────────────────────────────────
71
 
 
72
  async def require_auth_token(
73
  x_auth_token: Optional[str] = Header(default=None, alias="X-Auth-Token"),
74
  ) -> str:
@@ -100,4 +104,4 @@ async def require_auth_token(
100
  headers={"WWW-Authenticate": "Bearer"},
101
  )
102
 
103
- return user_id
 
19
 
20
  # ── Low-level JWT helpers ─────────────────────────────────────────────────────
21
 
22
+
23
  def extract_jwt_sub(access_token: str) -> str:
24
  """
25
  Extract the Supabase user id (JWT `sub`) while strictly verifying the signature.
 
32
  import jwt
33
 
34
  if not config.SUPABASE_JWT_SECRET:
35
+ raise RuntimeError(
36
+ "SUPABASE_JWT_SECRET is not configured. "
37
+ "Set it in your .env (Supabase β†’ Settings β†’ API β†’ JWT Settings)."
38
+ )
39
+
40
+ try:
41
+ payload = jwt.decode(
42
+ access_token,
43
+ key=config.SUPABASE_JWT_SECRET,
44
+ algorithms=["HS256"],
45
+ audience="authenticated",
46
+ )
47
+ except jwt.ExpiredSignatureError:
48
+ raise ValueError("JWT has expired")
49
+ except jwt.InvalidTokenError as e:
50
+ raise ValueError(f"Invalid JWT signature or payload: {e}")
 
51
 
52
  # Supabase uses `sub` for the user UUID.
53
  sub = payload.get("sub") or payload.get("user_id") or payload.get("uid")
54
  if not sub:
55
+ raise ValueError(
56
+ "JWT does not contain a user id claim (`sub` / `uid` / `user_id`)."
57
+ )
58
 
59
  return str(sub)
60
 
 
65
  return extract_jwt_sub(access_token) if access_token else None
66
  except Exception as e:
67
  import logging
68
+
69
+ logging.getLogger("morpheus.auth").debug("JWT extraction failed: %s", e)
70
  return None
71
 
72
 
73
  # ── FastAPI dependency ────────────────────────────────────────────────────────
74
 
75
+
76
  async def require_auth_token(
77
  x_auth_token: Optional[str] = Header(default=None, alias="X-Auth-Token"),
78
  ) -> str:
 
104
  headers={"WWW-Authenticate": "Bearer"},
105
  )
106
 
107
+ return user_id
backend/core/cache_manager.py CHANGED
@@ -21,7 +21,7 @@ import os
21
  import time
22
  from typing import Any, Dict, List, Optional
23
 
24
- log = logging.getLogger("nexus.cache")
25
 
26
  # ── Config ────────────────────────────────────────────────────────────────────
27
  TTL_BY_DOCTYPE = {
@@ -45,8 +45,8 @@ SHAREABLE_TYPES = {
45
  "reference_chart",
46
  }
47
 
48
- PREFIX_CACHE = "nexus:qcache"
49
- PREFIX_VERSION = "nexus:kb_version"
50
 
51
 
52
  def _get_redis():
 
21
  import time
22
  from typing import Any, Dict, List, Optional
23
 
24
+ log = logging.getLogger("morpheus.cache")
25
 
26
  # ── Config ────────────────────────────────────────────────────────────────────
27
  TTL_BY_DOCTYPE = {
 
45
  "reference_chart",
46
  }
47
 
48
+ PREFIX_CACHE = "morpheus:qcache"
49
+ PREFIX_VERSION = "morpheus:kb_version"
50
 
51
 
52
  def _get_redis():
backend/main.py CHANGED
@@ -4,6 +4,7 @@ backend/main.py β€” FastAPI entry point.
4
  Local: uvicorn backend.main:app --reload --port 8000
5
  Production: gunicorn -w 1 -k uvicorn.workers.UvicornWorker backend.main:app --bind 0.0.0.0:$PORT --timeout 120
6
  """
 
7
  import os
8
  import sys
9
  from slowapi import Limiter, _rate_limit_exceeded_handler
@@ -31,13 +32,12 @@ from dotenv import load_dotenv # noqa: E402
31
 
32
  load_dotenv()
33
 
34
- from backend.api import auth, corpus, ingest, query, admin,frontend_config # noqa: E402
35
  from backend.core.intent_classifier import get_intent_classifier_status # noqa: E402
36
 
37
  log = logging.getLogger("morpheus.main")
38
 
39
 
40
-
41
  @asynccontextmanager
42
  async def lifespan(app: FastAPI):
43
  log.info("MORPHEUS API starting")
@@ -47,13 +47,18 @@ async def lifespan(app: FastAPI):
47
  celery_process = None
48
  if os.getenv("AUTO_START_CELERY", "true").lower() == "true":
49
  try:
50
- celery_process = subprocess.Popen([
51
- sys.executable, "-m", "celery",
52
- "-A", "backend.core.tasks",
53
- "worker",
54
- "--pool=solo",
55
- "--loglevel=info",
56
- ])
 
 
 
 
 
57
  log.info("Celery worker auto-started (PID %s)", celery_process.pid)
58
  except Exception as exc:
59
  log.warning("Could not auto-start Celery worker: %s", exc)
@@ -73,26 +78,36 @@ async def lifespan(app: FastAPI):
73
 
74
 
75
  app = FastAPI(
76
- title="Morpheus RAG API", version="1.0.0", lifespan=lifespan,
77
- docs_url = "/docs" if os.getenv("DOCS_ENABLED", "true").lower() == "true" else None,
78
- redoc_url = "/redoc" if os.getenv("DOCS_ENABLED", "true").lower() == "true" else None,
 
 
79
  )
80
 
81
  # ── Rate limiting ─────────────────────────────────────────────────────────────
82
  app.state.limiter = limiter
83
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
84
 
85
- _origins = [o.strip() for o in os.getenv("ALLOWED_ORIGINS", "*").split(",") if o.strip()]
86
- app.add_middleware(CORSMiddleware, allow_origins=_origins,
87
- allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
 
 
88
 
89
- app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
90
  app.include_router(corpus.router, prefix="/api/v1/corpus", tags=["corpus"])
91
  app.include_router(ingest.router, prefix="/api/v1/ingest", tags=["ingest"])
92
- app.include_router(query.router, prefix="/api/v1/query", tags=["query"])
93
- app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])
94
  app.include_router(frontend_config.router, prefix="/api/v1/config", tags=["config"])
95
 
 
96
  @app.get("/health")
97
  def health():
98
  return {"status": "healthy"}
@@ -105,10 +120,12 @@ def health_details():
105
  "intent_classifier": get_intent_classifier_status(),
106
  }
107
 
 
108
  @app.get("/api/status")
109
  def status():
110
  return {"status": "ok", "service": "Morpheus RAG API", "version": "1.0.0"}
111
 
 
112
  # ── Static Frontend ───────────────────────────────────────────────────────────
113
  # Mount the entire frontend folder at the root of the app so it serves the index.html.
114
  # This makes it a self-contained single-container app for deployment.
@@ -120,9 +137,8 @@ if os.path.isdir(frontend_path):
120
  else:
121
  log.warning("Frontend directory not found at %s. API-only mode.", frontend_path)
122
 
123
- app.mount("/css", StaticFiles(directory="frontend/css"), name="css")
124
- app.mount("/js", StaticFiles(directory="frontend/js"), name="js")
125
 
126
- @app.get("/")
127
- async def serve_index():
128
- return FileResponse("frontend/index.html")
 
4
  Local: uvicorn backend.main:app --reload --port 8000
5
  Production: gunicorn -w 1 -k uvicorn.workers.UvicornWorker backend.main:app --bind 0.0.0.0:$PORT --timeout 120
6
  """
7
+
8
  import os
9
  import sys
10
  from slowapi import Limiter, _rate_limit_exceeded_handler
 
32
 
33
  load_dotenv()
34
 
35
+ from backend.api import auth, corpus, ingest, query, admin, frontend_config # noqa: E402
36
  from backend.core.intent_classifier import get_intent_classifier_status # noqa: E402
37
 
38
  log = logging.getLogger("morpheus.main")
39
 
40
 
 
41
  @asynccontextmanager
42
  async def lifespan(app: FastAPI):
43
  log.info("MORPHEUS API starting")
 
47
  celery_process = None
48
  if os.getenv("AUTO_START_CELERY", "true").lower() == "true":
49
  try:
50
+ celery_process = subprocess.Popen(
51
+ [
52
+ sys.executable,
53
+ "-m",
54
+ "celery",
55
+ "-A",
56
+ "backend.core.tasks",
57
+ "worker",
58
+ "--pool=solo",
59
+ "--loglevel=info",
60
+ ]
61
+ )
62
  log.info("Celery worker auto-started (PID %s)", celery_process.pid)
63
  except Exception as exc:
64
  log.warning("Could not auto-start Celery worker: %s", exc)
 
78
 
79
 
80
  app = FastAPI(
81
+ title="Morpheus RAG API",
82
+ version="1.0.0",
83
+ lifespan=lifespan,
84
+ docs_url="/docs" if os.getenv("DOCS_ENABLED", "true").lower() == "true" else None,
85
+ redoc_url="/redoc" if os.getenv("DOCS_ENABLED", "true").lower() == "true" else None,
86
  )
87
 
88
  # ── Rate limiting ─────────────────────────────────────────────────────────────
89
  app.state.limiter = limiter
90
  app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
91
 
92
+ _origins = [
93
+ o.strip() for o in os.getenv("ALLOWED_ORIGINS", "*").split(",") if o.strip()
94
+ ]
95
+ app.add_middleware(
96
+ CORSMiddleware,
97
+ allow_origins=_origins,
98
+ allow_credentials=True,
99
+ allow_methods=["*"],
100
+ allow_headers=["*"],
101
+ )
102
 
103
+ app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
104
  app.include_router(corpus.router, prefix="/api/v1/corpus", tags=["corpus"])
105
  app.include_router(ingest.router, prefix="/api/v1/ingest", tags=["ingest"])
106
+ app.include_router(query.router, prefix="/api/v1/query", tags=["query"])
107
+ app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])
108
  app.include_router(frontend_config.router, prefix="/api/v1/config", tags=["config"])
109
 
110
+
111
  @app.get("/health")
112
  def health():
113
  return {"status": "healthy"}
 
120
  "intent_classifier": get_intent_classifier_status(),
121
  }
122
 
123
+
124
  @app.get("/api/status")
125
  def status():
126
  return {"status": "ok", "service": "Morpheus RAG API", "version": "1.0.0"}
127
 
128
+
129
  # ── Static Frontend ───────────────────────────────────────────────────────────
130
  # Mount the entire frontend folder at the root of the app so it serves the index.html.
131
  # This makes it a self-contained single-container app for deployment.
 
137
  else:
138
  log.warning("Frontend directory not found at %s. API-only mode.", frontend_path)
139
 
140
+ # /css and /js are already served by the root StaticFiles mount above.
141
+ # Separate mounts removed to avoid routing conflicts.
142
 
143
+ # Note: StaticFiles(html=True) above already serves frontend/index.html for GET /
144
+ # The explicit route below is intentionally removed to avoid a dead route.
 
render.yaml CHANGED
@@ -1,24 +1,74 @@
1
  services:
2
  - type: web
3
- name: nexus-api
4
  runtime: python
5
- buildCommand: pip install -r backend/requirements.txt
6
  startCommand: gunicorn -w 1 -k uvicorn.workers.UvicornWorker backend.main:app --bind 0.0.0.0:$PORT --timeout 120
7
  envVars:
8
  - key: OPENROUTER_API_KEY
9
- sync: false # set manually in Render dashboard
10
  - key: SUPABASE_URL
11
  sync: false
12
  - key: SUPABASE_SERVICE_KEY
13
  sync: false
 
 
 
 
 
 
 
 
14
  - key: COHERE_API_KEY
15
  sync: false
16
  - key: MASTER_ADMIN_KEY
17
  sync: false
 
 
 
 
 
18
  - key: ALLOWED_ORIGINS
19
- value: "https://your-nexus.vercel.app" # ← update after Vercel deploy
20
  - key: DOCS_ENABLED
21
  value: "false"
22
  - key: LOG_LEVEL
23
  value: "INFO"
 
 
24
  healthCheckPath: /health
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  services:
2
  - type: web
3
+ name: morpheus-api
4
  runtime: python
5
+ buildCommand: pip install -r requirements.txt
6
  startCommand: gunicorn -w 1 -k uvicorn.workers.UvicornWorker backend.main:app --bind 0.0.0.0:$PORT --timeout 120
7
  envVars:
8
  - key: OPENROUTER_API_KEY
9
+ sync: false
10
  - key: SUPABASE_URL
11
  sync: false
12
  - key: SUPABASE_SERVICE_KEY
13
  sync: false
14
+ - key: SUPABASE_ANON_KEY
15
+ sync: false
16
+ - key: SUPABASE_JWT_SECRET
17
+ sync: false
18
+ - key: GROQ_API_KEY
19
+ sync: false
20
+ - key: GEMINI_API_KEY
21
+ sync: false
22
  - key: COHERE_API_KEY
23
  sync: false
24
  - key: MASTER_ADMIN_KEY
25
  sync: false
26
+ - key: REDIS_URL
27
+ fromService:
28
+ name: morpheus-redis
29
+ type: redis
30
+ property: connectionString
31
  - key: ALLOWED_ORIGINS
32
+ value: "https://your-morpheus.vercel.app"
33
  - key: DOCS_ENABLED
34
  value: "false"
35
  - key: LOG_LEVEL
36
  value: "INFO"
37
+ - key: AUTO_START_CELERY
38
+ value: "false"
39
  healthCheckPath: /health
40
+
41
+ - type: worker
42
+ name: morpheus-celery
43
+ runtime: python
44
+ buildCommand: pip install -r requirements.txt
45
+ startCommand: python -m celery -A backend.core.tasks worker --pool=solo --loglevel=info
46
+ envVars:
47
+ - key: OPENROUTER_API_KEY
48
+ sync: false
49
+ - key: SUPABASE_URL
50
+ sync: false
51
+ - key: SUPABASE_SERVICE_KEY
52
+ sync: false
53
+ - key: SUPABASE_ANON_KEY
54
+ sync: false
55
+ - key: SUPABASE_JWT_SECRET
56
+ sync: false
57
+ - key: GROQ_API_KEY
58
+ sync: false
59
+ - key: GEMINI_API_KEY
60
+ sync: false
61
+ - key: COHERE_API_KEY
62
+ sync: false
63
+ - key: REDIS_URL
64
+ fromService:
65
+ name: morpheus-redis
66
+ type: redis
67
+ property: connectionString
68
+ - key: LOG_LEVEL
69
+ value: "INFO"
70
+
71
+ databases:
72
+ - type: redis
73
+ name: morpheus-redis
74
+ plan: free