Skip to content
Merged
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
58 changes: 40 additions & 18 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
# Copy to .env and edit as needed. All variables are optional; defaults are fine for local dev.
# ─────────────────────────────────────────────────────────────────────────────
# DEV ENV TEMPLATE — root .env (loaded by docker compose)
# ─────────────────────────────────────────────────────────────────────────────
# Copy to `.env` (gitignored) and fill in real values. Docker compose reads
# `.env` automatically from the directory it runs in. No `.env.local` fallback.
#
# Phase 18d: we run against a real Supabase cloud project (`codetutor-dev`) in
# dev, CI, and E2E. There is no local Supabase stack anymore. Contributors
# without project creds should reach out to the repo owner — see
# docs/DEVELOPMENT.md → "First-time setup".
#
# Every value here is 12-factor config; nothing is hardcoded in source.
# ─────────────────────────────────────────────────────────────────────────────

# --- Backend ---
# ── Backend ──────────────────────────────────────────────────────────────────
PORT=4000
CORS_ORIGIN=http://localhost:5173

# Name of the pre-built polyglot runner image.
RUNNER_IMAGE=ai-code-editor-runner:latest
RUNNER_IMAGE=codetutor-ai-runner:latest

# Per-session idle timeout and how often the sweeper runs. Both in milliseconds.
# Per-session idle timeout + how often the sweeper runs. Both in milliseconds.
SESSION_IDLE_TIMEOUT_MS=120000
SESSION_SWEEP_INTERVAL_MS=45000

Expand All @@ -16,19 +28,29 @@ RUNNER_MEMORY_BYTES=536870912
RUNNER_NANO_CPUS=1000000000
RUN_TIMEOUT_MS=10000

# Backend-internal path where per-session workspaces live. Always a Linux
# path because the backend runs in a Linux container. Leave as the default
# when running via docker compose; override only if you've changed the bind
# mount in docker-compose.yml.
# WORKSPACE_ROOT=/workspace-root
# ── Supabase (codetutor-dev cloud project) ───────────────────────────────────
# From Supabase dashboard → Project Settings → Data API:
# Project URL → SUPABASE_URL
# From Supabase dashboard → Project Settings → API Keys:
# Publishable key → SUPABASE_ANON_KEY / VITE_SUPABASE_ANON_KEY
# Secret key → SUPABASE_SERVICE_ROLE_KEY (sensitive, server-side only)
# From Supabase dashboard → Project Settings → Database → Connection string →
# Transaction pooler (port 6543), password filled in → DATABASE_URL
#
# JWT verification is asymmetric (JWKS). No shared secret env var.
SUPABASE_URL=https://<your-dev-project-ref>.supabase.co
# E2E helper (Playwright) reads SUPABASE_ANON_KEY without the VITE_ prefix.
SUPABASE_ANON_KEY=<your-dev-publishable-key>
SUPABASE_SERVICE_ROLE_KEY=<your-dev-secret-key>
DATABASE_URL=postgresql://postgres.<ref>:<db-password>@aws-0-<region>.pooler.supabase.com:6543/postgres

# Escape hatch: host-side absolute path that backs WORKSPACE_ROOT.
# Normally discovered automatically at startup via Docker self-inspect, which
# works identically on macOS, Linux, and Windows. Only set this when running
# the backend directly on the host (outside docker compose).
# Unix example: /absolute/path/to/AICodeEditor/temp/sessions
# Windows example: C:\path\to\AICodeEditor\temp\sessions
# WORKSPACE_ROOT_HOST=
# ── Frontend (Vite dev server inside docker compose) ─────────────────────────
# When the frontend container runs `vite dev`, these values flow via compose
# env interpolation. For host-run Vite (`cd frontend && npm run dev`), Vite
# instead reads `frontend/.env.development.local` — populate that file too.
VITE_BACKEND_URL=http://backend:4000
VITE_SUPABASE_URL=https://<your-dev-project-ref>.supabase.co
VITE_SUPABASE_ANON_KEY=<your-dev-publishable-key>

# --- Frontend (only used when running Vite directly) ---
VITE_BACKEND_URL=http://localhost:4000
# ── Local OpenAI key (optional; can also live in browser localStorage) ───────
# OPENAI_API_KEY=sk-...
53 changes: 53 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# ─────────────────────────────────────────────────────────────────────────────
# PRODUCTION ENV TEMPLATE
# ─────────────────────────────────────────────────────────────────────────────
# Copy to `.env.production` (gitignored) and fill in real values from the
# `codetutor-prod` Supabase project. Deploy with:
#
# docker compose --env-file=.env.production up -d --build
#
# Every value here is 12-factor config. Same image, different env file =
# different environment. No values are hardcoded in source.
# ─────────────────────────────────────────────────────────────────────────────

# ── Backend ──────────────────────────────────────────────────────────────────
BACKEND_PORT=4000
# Bind the published port to the interface your load balancer / ingress reaches.
# "0.0.0.0" is fine behind a VPC + LB; do NOT use on a raw public host.
BACKEND_BIND=0.0.0.0

# CORS allow-list for the browser origin. Must exactly match the frontend's
# public URL (scheme + host + port).
CORS_ORIGIN=https://codetutor.example.com

# The ExecutionBackend seam. `local-docker` is the only impl today; cloud
# variants (ecs-fargate / aks / aci) will plug in here without app changes.
EXECUTION_BACKEND=local-docker

# Runner-image tag built out-of-band by CI and pushed to your registry.
RUNNER_IMAGE=your-registry.example.com/codetutor-ai-runner:stable

# Rate limits. The config.ts defaults (30 / 120 / 60 per minute) are the
# production posture. Only override if you've measured and need it.
# SESSION_CREATE_RATE_LIMIT_MAX=30
# MUTATION_RATE_LIMIT_MAX=120
# AI_RATE_LIMIT_MAX=60

# ── Supabase (codetutor-prod) ────────────────────────────────────────────────
# From the codetutor-prod dashboard — see .env.example for where to click.
SUPABASE_URL=https://<your-prod-project-ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<your-prod-secret-key>
DATABASE_URL=postgresql://postgres.<ref>:<db-password>@aws-0-<region>.pooler.supabase.com:6543/postgres

# ── Frontend (Vite build) ────────────────────────────────────────────────────
# Prod deploys serve a static bundle via nginx. FRONTEND_BUILD_TARGET=prod
# tells docker-compose.yml to pick the `prod` stage of frontend/Dockerfile
# (build-once + nginx) versus the default `dev` stage (Vite dev server).
# VITE_* values below are baked in at `docker compose build` time — rebuild
# is required after changing them.
FRONTEND_BUILD_TARGET=prod
FRONTEND_PORT=5173
FRONTEND_BIND=0.0.0.0
VITE_BACKEND_URL=https://api.codetutor.example.com
VITE_SUPABASE_URL=https://<your-prod-project-ref>.supabase.co
VITE_SUPABASE_ANON_KEY=<your-prod-publishable-key>
11 changes: 11 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ jobs:
name: Playwright (chromium)
runs-on: ubuntu-latest
timeout-minutes: 15
# Phase 18d: cloud-only Supabase model. These feed both docker compose
# (which reads process env for ${VAR} interpolation in docker-compose.yml)
# and the Playwright specs (fixtures + auth.spec.ts read process.env).
# Values come from the codetutor-dev project — see docs/DEVELOPMENT.md.
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
steps:
- uses: actions/checkout@v4

Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
node_modules/
dist/
build/
# Env files: only *.example templates are committed. Real values live in
# gitignored files populated on the deploy target (or by CI secrets).
.env
.env.local
.env.production
.env.*.local
.DS_Store
*.log
*.tsbuildinfo
Expand All @@ -19,6 +23,7 @@ temp/sessions/*
.vscode/
.idea/
.claude/
.agents/

# Playwright E2E artifacts (e2e/ has its own .gitignore; these catch stray
# outputs when playwright is invoked from the repo root).
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
**Learn to code with an AI tutor that guides you — without giving away the answers.**
<br />
Write code, run it in a sandboxed environment, and get structured help when you're stuck.
<br />
No account needed. Runs entirely on your machine.

<br />
<br />
Expand Down Expand Up @@ -106,7 +104,7 @@ No account needed. Runs entirely on your machine.
</td>
<td>

[Git](https://git-scm.com/downloads) and [Docker Desktop](https://www.docker.com/products/docker-desktop/) (running). Optional: [OpenAI API key](https://platform.openai.com/api-keys) for the AI tutor.
[Git](https://git-scm.com/downloads), [Docker Desktop](https://www.docker.com/products/docker-desktop/) (running), and Supabase credentials — request the dev bundle from the repo owner or point at your own fork's project (see [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#first-time-setup-auth-stack)). Optional: [OpenAI API key](https://platform.openai.com/api-keys) for the AI tutor.

</td>
</tr>
Expand All @@ -117,6 +115,8 @@ No account needed. Runs entirely on your machine.
```bash
git clone https://github.com/msrivas-7/CodeTutor-AI.git
cd codetutor-ai
cp .env.example .env # fill in Supabase creds
cp frontend/.env.development.example frontend/.env.development.local
./start.sh # first build ~2-3 min, then ~10s
```

Expand All @@ -125,6 +125,8 @@ cd codetutor-ai
```powershell
git clone https://github.com/msrivas-7/CodeTutor-AI.git
cd codetutor-ai
Copy-Item .env.example .env # fill in Supabase creds
Copy-Item frontend/.env.development.example frontend/.env.development.local
powershell -ExecutionPolicy Bypass -File .\start.ps1
```

Expand Down
24 changes: 24 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"express": "^4.19.2",
"express-rate-limit": "^8.3.2",
"helmet": "^8.1.0",
"jose": "^6.2.2",
"nanoid": "^5.0.7",
"postgres": "^3.4.9",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
40 changes: 40 additions & 0 deletions backend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,44 @@ export const config = {
),
mutationMax: num(process.env.MUTATION_RATE_LIMIT_MAX, 120),
},

// Phase 18a: Supabase Auth. `url` points at the Supabase API root (GoTrue
// lives under /auth/v1). The backend does NOT use an anon/service-role
// key — it only verifies access tokens coming from the browser, by
// fetching the JWKS from the auth server. JWKS verification needs no
// shared secret; it's asymmetric.
//
// 12-factor: no default. The value must come from env (.env / .env.production).
// Missing env at boot is a deployment misconfig; assertConfigValid() fails
// fast rather than silently pointing at a wrong URL.
supabase: {
url: process.env.SUPABASE_URL,
},

// Phase 18b: Postgres for per-user state (preferences, progress, editor
// project). Points at the Supabase-managed Postgres for the current
// environment (transaction pooler URL from Project Settings → Database).
databaseUrl: process.env.DATABASE_URL,
} as const;

export function assertConfigValid(): void {
if (!config.supabase.url || config.supabase.url.trim() === "") {
throw new Error(
"[config] SUPABASE_URL is required. Populate `.env` from `.env.example` " +
"with your codetutor-dev / codetutor-prod project URL.",
);
}
try {
new URL(config.supabase.url);
} catch {
throw new Error(
`[config] SUPABASE_URL is not a valid URL: ${config.supabase.url}`,
);
}
if (!config.databaseUrl || config.databaseUrl.trim() === "") {
throw new Error(
"[config] DATABASE_URL is required. Populate `.env` from `.env.example` " +
"with your project's transaction-pooler connection string.",
);
}
}
44 changes: 44 additions & 0 deletions backend/src/db/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import postgres, { type Sql } from "postgres";
import { config } from "../config.js";

// Phase 18b: one postgres.js connection pool shared across all db/* modules.
// Lazy init so unit tests that don't touch the DB never open a connection.
// DATABASE_URL points at the Supabase transaction pooler (port 6543) for the
// current environment — see config.ts + docs/DEVELOPMENT.md.

let pool: Sql | null = null;

export function db(): Sql {
if (pool) return pool;
const url = config.databaseUrl;
if (!url) {
throw new Error(
"[db] DATABASE_URL is not set; assertConfigValid() should have caught " +
"this at boot. Check your env file.",
);
}
pool = postgres(url, {
max: 10,
idle_timeout: 30,
connect_timeout: 10,
// Supabase's transaction pooler (port 6543) recycles connections between
// transactions and does not support prepared statements. Without this
// flag we see "prepared statement does not exist" errors under load.
prepare: false,
// Let application errors bubble as postgres.PostgresError so route
// handlers can translate them to HTTP codes (unique_violation → 409, etc).
onnotice: () => {},
});
return pool;
}

export async function closeDb(): Promise<void> {
if (!pool) return;
await pool.end({ timeout: 5 });
pool = null;
}

// Test-only: lets specs reset the pool between runs.
export function __resetDbForTests(): void {
pool = null;
}
Loading
Loading