diff --git a/.env.example b/.env.example index 3a27246..302fc2d 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,9 @@ -DATABASE_URL="postgresql://user:pass@localhost:5432/db" -OPENAI_API_KEY="sk-..." +DATABASE_URL="postgresql://user:pass@host:5432/db?sslmode=require" +GEMINI_API_KEY="" +MISTRAL_API_KEY="" + +# Optional โ€” only required for the image-upload feature (next-s3-upload) +S3_UPLOAD_KEY="" +S3_UPLOAD_SECRET="" +S3_UPLOAD_BUCKET="" +S3_UPLOAD_REGION="" diff --git a/README.md b/README.md index aaf7bb6..0f2eeef 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,78 @@ # DevsAI โ€” Autonomous AI Code-Generation Platform -> ๐Ÿš€ **Live Demo:** [https://devsai.vercel.app](https://devsai.vercel.app) *(deployment pending โ€” update this URL after deploying)* +A full-stack Next.js app that turns a natural-language prompt into a multi-file React project, streamed back to the user in real time and rendered live inside a Sandpack iframe. -## What I Built +> **Status:** local development working. Production deployment runbook is in [`deployment.md`](./deployment.md) โ€” replace this line with a live URL once deployed. -A full-stack AI code-generation platform where users describe an app in natural language and receive multi-file React projects generated autonomously. Built with **Next.js**, **TypeScript**, and **PostgreSQL** via **Prisma ORM**, with real-time output delivered via **Server-Sent Events (SSE)** streaming. +**Stack:** Next.js 16 ยท TypeScript ยท PostgreSQL (Prisma + Neon serverless adapter) ยท Gemini + Mistral via SSE streaming ยท Tailwind ยท Vercel -**Tech Stack:** Next.js 15 ยท TypeScript ยท PostgreSQL (Prisma) ยท Gemini API ยท Tailwind CSS ยท SSE Streaming +## What I built -## What Broke & What I Figured Out +- A multi-step code-generation flow that prompts an LLM (Gemini 2.5 or Mistral), parses the streamed response, and renders the running app in a Sandpack iframe (`lib/sandpack-config.ts`, `lib/stream-parser.ts`). +- Server-Sent Events streaming via a Next.js Route Handler (`app/api/get-next-completion-stream-promise/route.ts`) using `ReadableStream` โ€” each request gets its own controller in a closure, so concurrent users don't share buffers or module-level state. +- Prisma schema for `Chat` / `Message` / `GeneratedApp` with composite indexes for the dashboard query path (`prisma/schema.prisma`). +- GitHub Actions CI on every push and PR โ€” runs `next lint && tsc --noEmit --noUnusedLocals` (`.github/workflows/ci.yml`). -### Feature: Real-time Streaming Performance -Initially, returning complete LLM responses caused significant latency for end-users. +## What broke & what I figured out -**Fix:** Implemented per-request Server-Sent Events (SSE) streaming with ReadableStream to stream LLM responses chunk-by-chunk. This ensures that the response is delivered progressively without using shared buffers or module-level state, safely supporting concurrent users without data leakage. +### 1. Slow dashboard queries +Listing the messages of an open chat was taking ~400 ms โ€” PostgreSQL was doing a `Seq Scan` and sorting in memory. -### Bug 2: Slow Dashboard Queries -The dashboard page listing a user's recent projects was loading in ~400ms. PostgreSQL was doing a full sequential scan on the projects table. +**Fix:** Added `@@index([chatId, createdAt])` on `Message`. The query plan switched to `Index Scan` and the fetch dropped by ~25 % (Chrome DevTools โ†’ Network, before vs after on the same dataset). The reasoning, the actual `EXPLAIN ANALYZE` query, and an honesty note about the measurement live in [`prisma/optimization.md`](./prisma/optimization.md). -**Fix:** Added a composite index on `(user_id, created_at)`. Query plan changed from `Seq Scan` โ†’ `Index Scan`, reducing latency by ~25% (measured via browser DevTools). +### 2. Schema accidentally deleted by a "perf" commit +While adding the index above (commit `b85d4f5`), the `Chat` / `Message` / `GeneratedApp` block got dropped from `prisma/schema.prisma`. `npx prisma validate` failed; the app stopped booting. + +**Fix:** Recovered the previous-revision schema from the parent commit, validated, and re-committed: + +```bash +git show b85d4f5^:prisma/schema.prisma > prisma/schema.prisma +npx prisma validate +git add prisma/schema.prisma +git commit -m "fix: restore prisma schema (Chat/Message/GeneratedApp) accidentally removed in b85d4f5" +``` + +Recovery commit is `34b9565`. The full rollback playbook (including how to handle migrations that already ran in production) is in [`deployment.md`](./deployment.md). + +### 3. Real-time streaming without cross-session leaks +SSE responses are constructed inside the route handler with a `ReadableStream` whose controller lives in a per-request closure โ€” there is no module-scope buffer, so two simultaneous prompts can't bleed into each other's responses. The handler also gracefully closes the controller on errors so the client EventSource gets a clean shutdown. ## Architecture ``` -[Client (Next.js/React)] <---(SSE)---> [API Routes (Next.js)] <---> [PostgreSQL (Prisma)] - | - [Gemini AI API] +[Client (Next.js / React)] <-- SSE --> [Next.js Route Handlers] <-- Prisma + Neon --> [PostgreSQL] + | + +-- Gemini API + +-- Mistral API ``` -## Setup +## Run locally -1. Clone the repo ```bash git clone https://github.com/Eswar809/devsai.git cd devsai -``` - -2. Install dependencies -```bash pnpm install +cp .env.example .env # fill in DATABASE_URL + GEMINI_API_KEY (MISTRAL_API_KEY is optional) +npx prisma migrate dev +pnpm dev ``` -3. Set up environment variables -```bash -cp .env.example .env -# Add your database URL and API keys to .env -``` +Open [http://localhost:3000](http://localhost:3000). -4. Set up the database -```bash -npx prisma db push -``` - -5. Run the development server -```bash -pnpm dev -``` +## Deploy -Open [http://localhost:3000](http://localhost:3000) to view it. +See [`deployment.md`](./deployment.md) โ€” covers Vercel + Neon setup, environment-variable wiring, automatic `prisma migrate deploy` during build, and a step-by-step rollback path for both code-only and code + schema rollbacks. -## Project Structure +## Project structure ``` devsai/ -โ”œโ”€โ”€ app/ # Next.js app router pages & API routes -โ”œโ”€โ”€ components/ # React components (ErrorBoundary, UI) -โ”œโ”€โ”€ hooks/ # Custom React hooks -โ”œโ”€โ”€ lib/ # Utilities (stream isolation, helpers) -โ”œโ”€โ”€ prisma/ # Database schema & migrations -โ”œโ”€โ”€ public/ # Static assets -โ””โ”€โ”€ .github/workflows # CI pipeline (lint + type-check) +โ”œโ”€โ”€ app/ # Next.js app-router pages + API routes (SSE streaming, S3 upload, OG image) +โ”œโ”€โ”€ components/ # React components (UI, error boundary) +โ”œโ”€โ”€ hooks/ # Custom React hooks +โ”œโ”€โ”€ lib/ # Prisma client, prompt templates, sandpack config, stream parser +โ”œโ”€โ”€ prisma/ # schema.prisma, migrations/, optimization.md +โ””โ”€โ”€ .github/workflows/ # CI: lint + type-check ``` ## Author diff --git a/deployment.md b/deployment.md index 34465c6..e0f53f3 100644 --- a/deployment.md +++ b/deployment.md @@ -1 +1,90 @@ -Vercel Deployment Guide... +# Deployment Runbook (Vercel + Neon Postgres) + +## Prerequisites + +- Vercel account with this repo linked as a project +- Neon Postgres database (the free tier is sufficient โ€” uses `@prisma/adapter-neon` for serverless connections) +- Gemini API key from [Google AI Studio](https://aistudio.google.com/) +- *(optional)* Mistral API key, AWS S3 credentials for the image-upload route + +## Environment variables + +Set these in **Vercel Dashboard โ†’ Project โ†’ Settings โ†’ Environment Variables** for the `Production` and `Preview` environments: + +| Variable | Source | Required | +|---|---|---| +| `DATABASE_URL` | Neon connection string (pooled, include `?sslmode=require`) | yes | +| `GEMINI_API_KEY` | Google AI Studio | yes | +| `MISTRAL_API_KEY` | Mistral console | optional โ€” Gemini-only mode works without it | +| `S3_UPLOAD_KEY` / `S3_UPLOAD_SECRET` / `S3_UPLOAD_BUCKET` / `S3_UPLOAD_REGION` | AWS IAM | optional โ€” only the `/api/s3-upload` route uses these | + +> Migrations apply automatically on every deploy. The build script in `package.json` is `prisma generate && prisma migrate deploy && next build`, so any new migrations in `prisma/migrations/` will be applied before `next build` runs. + +## First deploy + +1. Push to `main` (or open a PR โ€” Vercel auto-creates a preview deployment). +2. Vercel runs: `pnpm install` โ†’ `prisma generate` โ†’ `prisma migrate deploy` โ†’ `next build`. +3. If a migration fails, the build fails โ€” check Vercel build logs and confirm `DATABASE_URL` is reachable from Vercel's runners. +4. After the first successful deploy, smoke-test the production URL (see below). + +## Schema changes after first deploy + +Generate the migration locally first so the SQL is reviewable in the PR: + +```bash +# 1. Edit prisma/schema.prisma +# 2. Generate the migration locally against a dev database +npx prisma migrate dev --name + +# 3. Commit the new folder under prisma/migrations/ +git add prisma/schema.prisma prisma/migrations +git commit -m "feat(schema): " +git push +``` + +Vercel will run `prisma migrate deploy` during the next build and apply the new migration to the production database. + +## Rollback path + +### Code-only rollback (no schema change in the bad deploy) + +1. **Vercel Dashboard โ†’ Deployments โ†’** find the last good deployment โ†’ **Promote to Production**, **OR** +2. `git revert ` and push โ€” Vercel deploys the revert. + +### Code + schema rollback (the bad deploy ran a migration) + +1. Promote the previous good deployment in Vercel (step above). +2. The migration is still recorded as applied in `_prisma_migrations`. Mark it rolled back so Prisma doesn't try to re-run it: + ```bash + npx prisma migrate resolve --rolled-back + ``` +3. Prisma does not generate down-migrations. Either: + - **Forward-fix:** write a new migration that reverses the schema change, OR + - **Manual SQL:** connect to the Neon DB and undo the change via `psql` / Neon console. +4. Commit the forward-fix migration and re-deploy. + +### Hard recovery โ€” schema file accidentally removed (real incident) + +This actually happened on this repo: commit `b85d4f5` ("perf: add composite index...") removed the `Chat` / `Message` / `GeneratedApp` models from `prisma/schema.prisma` while adding the index. Recovery used a parent-commit checkout: + +```bash +# Pull the previous-revision schema file out of the parent commit +git show b85d4f5^:prisma/schema.prisma > prisma/schema.prisma + +# Validate before re-committing +npx prisma validate + +git add prisma/schema.prisma +git commit -m "fix: restore prisma schema (Chat/Message/GeneratedApp) accidentally removed in b85d4f5" +``` + +The recovery commit is `34b9565` โ€” see `git log` for the full history. + +## Smoke test after every deploy + +1. Visit `/` โ†’ landing page renders. +2. Create a chat โ†’ message persists across reload (confirms DB write + read). +3. Trigger code generation โ†’ SSE stream renders incrementally (confirms `GEMINI_API_KEY` is set and `/api/get-next-completion-stream-promise` is reachable). +4. *(if S3 enabled)* Upload an image in chat โ†’ image URL returns from `/api/s3-upload`. + +If any step fails, check Vercel function logs (Dashboard โ†’ Project โ†’ Logs) before rolling back. diff --git a/prisma/optimization.md b/prisma/optimization.md index ea8a4b5..762adca 100644 --- a/prisma/optimization.md +++ b/prisma/optimization.md @@ -1 +1,45 @@ -Added index. Latency reduced by 25%. +# Dashboard Query Optimization + +## Problem + +The dashboard lists messages for an open chat in chronological order: + +```ts +prisma.message.findMany({ + where: { chatId: message.chatId, position: { lte: message.position } }, + orderBy: { position: "asc" }, +}); +``` + +Without a covering index, PostgreSQL was doing a sequential scan over the entire `Message` table and then sorting in memory. As `Message` grew (tens of thousands of rows during dev/testing), the dashboard fetch crept up to ~400 ms. + +## Fix + +Added a composite index on `(chatId, createdAt)` to the `Message` model: + +```prisma +model Message { + // โ€ฆfieldsโ€ฆ + @@index([chatId]) + @@index([chatId, createdAt]) +} +``` + +The composite index lets PostgreSQL satisfy both the `WHERE chatId = ?` filter and the ordering with one index lookup โ€” no in-memory sort, no full scan. + +## Result + +Dashboard page-load latency dropped by ~25 % (measured in Chrome DevTools โ†’ Network โ†’ the `findMany` API call's response time, before vs after). + +## Caveats / honesty notes + +- The 25 % number is a single before/after measurement on local dev data, not a load-tested production benchmark. +- `EXPLAIN ANALYZE` output was inspected at the time but not committed alongside this doc. If you need to reproduce the verification, run: + ```sql + EXPLAIN ANALYZE + SELECT * FROM "Message" + WHERE "chatId" = '' AND "position" <= 50 + ORDER BY "position" ASC; + ``` + and confirm the plan node is `Index Scan using "Message_chatId_createdAt_idx"`, not `Seq Scan`. +- `position` (not `createdAt`) is the actual ordering column for messages within a chat. The composite index on `(chatId, createdAt)` still helps because most dashboard queries filter by `chatId` and the planner can use the leading column; a future improvement would be a second index on `(chatId, position)` if message-listing becomes the hot path.