Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ docker-compose*.yml
cloudbuild.yaml

# Drizzle
drizzle/

# Scripts
install.sh
Expand Down
12 changes: 12 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,20 @@ GEMINI_3_PRO_API_KEY=your_gemini_3_pro_api_key_here
# If not set, the `search` tool is skipped and the LLM answers from its own knowledge.
TAVILY_API_KEY=your_tavily_api_key

# Firecrawl API (https://firecrawl.dev)
FIRECRAWL_API_KEY=your_firecrawl_api_key

# Supabase Credentials
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL_HERE
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY_HERE
SUPABASE_SERVICE_ROLE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY_HERE
DATABASE_URL=postgresql://postgres:[YOUR-POSTGRES-PASSWORD]@[YOUR-SUPABASE-DB-HOST]:[PORT]/postgres

# Vercel Sandbox (MicroVM Infrastructure)
# Required for OIDC-based microVM authentication
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
Comment on lines +38 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Reorder environment variables to alphabetical order.

The static analysis tool (dotenv-linter) reports that VERCEL_TEAM_ID (line 38) should appear before VERCEL_TOKEN (line 36) to maintain alphabetical/conventional ordering.

🔧 Proposed reordering
-# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
-VERCEL_TOKEN=your_vercel_token
-# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
-VERCEL_TEAM_ID=your_vercel_team_id
+# VERCEL_PROJECT_ID: Found in Vercel Dashboard > [Project] > Settings > General (starts with 'prj_')
+VERCEL_PROJECT_ID=your_vercel_project_id
+# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
+VERCEL_TEAM_ID=your_vercel_team_id
+# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
+VERCEL_TOKEN=your_vercel_token
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 38-38: [UnorderedKey] The VERCEL_TEAM_ID key should go before the VERCEL_TOKEN key

(UnorderedKey)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.local.example around lines 35 - 38, Reorder the environment variables
in the file to maintain alphabetical order as required by dotenv-linter. Move
the VERCEL_TEAM_ID variable block (including its comment lines) to appear before
the VERCEL_TOKEN variable block. This ensures that VERCEL_TEAM_ID comes before
VERCEL_TOKEN alphabetically, satisfying the linting rule for conventional
environment variable ordering.

Source: Linters/SAST tools

# VERCEL_PROJECT_ID: Found in Vercel Dashboard > [Project] > Settings > General (starts with 'prj_')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
VERCEL_PROJECT_ID=your_vercel_project_id
97 changes: 97 additions & 0 deletions CLOUD_DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Cloud Deployment Guide for QCX

This guide provides instructions for deploying the QCX stack (Next.js, PostgreSQL with pgvector/PostGIS, and Qdrant) to various cloud platforms.

## Infrastructure Overview

The application consists of:
1. **QCX Web App**: Next.js application running on Bun.
2. **PostgreSQL**: Database with `pgvector` and `PostGIS` extensions.
3. **Qdrant**: High-performance vector database (optional, for advanced vector search).

---

## 1. Deploying with Docker Compose (VPS / EC2 / Compute Engine)

The easiest way to deploy the entire stack is using `docker-compose`.

1. **Clone the repository** on your server.
2. **Create a `.env` file** based on the environment variables in `docker-compose.yaml`.
3. **Run the stack**:
```bash
docker-compose up -d --build
```

---

## 2. Deploying to Render

Render is a great choice for managed services.

### PostgreSQL (Managed)
1. Create a **New PostgreSQL** instance on Render.
2. **Note**: Standard Render Postgres does not include `pgvector` or `PostGIS` by default on all plans. You may need to use a Docker-based Postgres on Render or ensure your plan supports these extensions.
3. If using Render's managed Postgres, run the extensions command manually via a SQL client:
```sql
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS vector;
```

### Web Service
1. Create a **New Web Service** pointing to your repository.
2. Select **Docker** as the runtime.
3. Specify the **Dockerfile path** as `Dockerfile`.
4. Add environment variables:
* `DATABASE_URL`: Your Render Postgres connection string.
* `EXECUTE_MIGRATIONS`: `true`
* `NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN`: Your Mapbox token.
* (Add other necessary API keys like Google, xAI, etc.)

### Qdrant (Optional)
1. Create a **New Private Service** or **Web Service**.
2. Select **Docker** as the runtime.
3. Use the image: `qdrant/qdrant:latest`.
4. Set `QDRANT_URL` in your Web Service to point to this service.

---

## 3. Deploying to Google Cloud (GCP)

### Cloud Run
1. **Build and Push** the image to Google Artifact Registry:
```bash
docker build -t gcr.io/YOUR_PROJECT/qcx .
docker push gcr.io/YOUR_PROJECT/qcx
```
2. **Deploy to Cloud Run**:
```bash
gcloud run deploy qcx --image gcr.io/YOUR_PROJECT/qcx --platform managed
```

### Cloud SQL
1. Create a **Cloud SQL for PostgreSQL** instance (version 15+).
2. Cloud SQL supports both `pgvector` and `postgis`. Enable them via:
```sql
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS vector;
```

---

## 4. Vector Database Options

QCX is configured to support two vector database options:

1. **PostgreSQL (pgvector)**: Integrated into the main database. Best for smaller datasets and simplicity.
2. **Qdrant**: Dedicated vector database. Recommended for large-scale production use cases requiring high performance and advanced filtering.

To use Qdrant, ensure the `QDRANT_URL` environment variable is set in your application environment.

---

## 5. Security Checklist

* [ ] Change default passwords in `docker-compose.yaml`.
* [ ] Use SSL for database connections in production (`ssl=true` in `DATABASE_URL`).
* [ ] Set `NODE_ENV=production`.
* [ ] Ensure all sensitive API keys are stored as secrets, not committed to code.
19 changes: 15 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ FROM oven/bun:1.3.5-alpine AS runner
WORKDIR /app

# Create non-root group and user, with nextjs belonging to nodejs group
RUN addgroup -g 1001 -S nodejs \
&& adduser -S -u 1001 -G nodejs nextjs
RUN addgroup -g 1001 -S nodejs && adduser -S -u 1001 -G nodejs nextjs

# Environment variables
ENV NODE_ENV=production
Expand All @@ -52,15 +51,27 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy public folder
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

# Copy migration files and entrypoint
# Note: In standalone mode, we need to ensure all migration dependencies are available.
# We copy the entire project source temporarily to a different folder if needed,
# or just ensure drizzle and lib/db are available for the migration script.
COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
COPY --from=builder --chown=nextjs:nodejs /app/lib/db/migrate.ts ./lib/db/migrate.ts
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
COPY --chown=nextjs:nodejs docker-entrypoint.sh ./docker-entrypoint.sh
RUN chmod +x docker-entrypoint.sh

# Switch to non-root user
USER nextjs

# Expose port
EXPOSE 3000

# Health check (uses Bun to fetch; adjust /api/health if your endpoint differs)
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD ["bun", "--eval", "fetch('http://localhost:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"]
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 CMD ["bun", "--eval", "fetch('http://localhost:3000/api/health').then(r => r.ok ? process.exit(0) : process.exit(1)).catch(() => process.exit(1))"]

# Use entrypoint script to handle migrations
ENTRYPOINT ["./docker-entrypoint.sh"]

# Run the standalone server with Bun (fully compatible in Bun 1.3+)
CMD ["bun", "server.js"]
21 changes: 21 additions & 0 deletions Dockerfile.db
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM postgres:16-alpine

# Install build dependencies and pgvector
RUN apk add --no-cache --virtual .build-deps \
git \
build-base \
clang15 \
llvm15-dev \
postgis-dev \
postgresql-dev \
&& git clone --branch v0.8.0 https://github.com/pgvector/pgvector.git /tmp/pgvector \
&& cd /tmp/pgvector \
&& make \
&& make install \
&& apk add --no-cache postgis \
&& rm -rf /tmp/pgvector \
&& apk del .build-deps

# Use the default entrypoint
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]
33 changes: 32 additions & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Section } from '@/components/section'
import { FollowupPanel } from '@/components/followup-panel'
import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, type DrawnFeature } from '@/lib/agents'
import { writer } from '@/lib/agents/writer'
import { saveChat, getSystemPrompt } from '@/lib/actions/chat'
import { saveChat, getSystemPrompt, generateReportContext } from '@/lib/actions/chat'
import { Chat, AIMessage } from '@/lib/types'
import { UserMessage } from '@/components/user-message'
import { BotMessage } from '@/components/message'
Expand All @@ -27,6 +27,7 @@ import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
import { MapQueryHandler } from '@/components/map/map-query-handler'
import { SandboxSection } from '@/components/sandbox-section'

// Define the type for related queries
type RelatedQueries = {
Expand All @@ -50,6 +51,20 @@ async function submit(formData?: FormData, skip?: boolean) {
console.error('Failed to parse drawnFeatures:', e);
}

if (action === 'generate_report_context') {
const messagesString = formData?.get('messages');
if (typeof messagesString !== 'string') {
return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' };
}
try {
const messages = JSON.parse(messagesString) as AIMessage[];
return await generateReportContext(messages);
} catch (e) {
console.error('Failed to parse messages for report context:', e);
return { title: 'QCX Intelligence Analysis', summary: 'Automated executive summary is currently unavailable.' };
}
}

if (action === 'resolution_search') {
const file_mapbox = formData?.get('file_mapbox') as File;
const file_google = formData?.get('file_google') as File;
Expand Down Expand Up @@ -897,6 +912,22 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
),
isCollapsed: isCollapsed.value
}
case 'sandbox': {
const logs = createStreamableValue(toolOutput.logs)
logs.done(toolOutput.logs)
return {
id,
component: (
<SandboxSection
logs={logs.value}
previewUrl={toolOutput.previewUrl}
exitCode={toolOutput.exitCode}
error={toolOutput.error}
/>
),
isCollapsed: isCollapsed.value
}
}
default:
console.warn(
`Unhandled tool result in getUIStateFromAIState: ${name}`
Expand Down
Loading