diff --git a/RELEASES.md b/RELEASES.md
new file mode 100644
index 0000000..32b367b
--- /dev/null
+++ b/RELEASES.md
@@ -0,0 +1,48 @@
+# Releases
+
+## v1.0.0 — Production Release (August 2026)
+
+DevForge reaches v1.0.0 with a complete AI-powered developer platform and
+engineering workspace.
+
+### Highlights
+
+- **Project management** — Kanban boards, milestones, roadmaps, task
+ dependencies, labels and full-text search.
+- **GitHub integration** — OAuth connection, repository import/sync,
+ pull request views, commit history and webhook-driven updates.
+- **Real-time collaboration** — Socket.io-powered notifications, team
+ chat, typing indicators and online presence.
+- **Analytics dashboard** — Velocity, health, contributor breakdowns
+ and repository activity, with weekly developer metrics snapshots.
+- **AI repository analyzer** — Architecture, code quality, security and
+ documentation scores with strengths, risks and recommendations.
+- **AI code review** — Severity-classified findings with a review score,
+ run directly on pull request diffs.
+- **AI documentation generator** — Preview-and-approve README and docs
+ generation; approvals commit directly to GitHub.
+- **AI engineering assistant** — Streamed, repo-scoped Q&A grounded in
+ your indexed repository chunks.
+- **DevOps** — Docker Compose full-stack (postgres, api, web, ai, nginx),
+ Prometheus metrics, CI quality gate with security auditing.
+
+### Test coverage
+
+470 passing tests across four stacks:
+
+| Stack | Tests | Files |
+| --- | --- | --- |
+| API (Express) | 220 | 17 |
+| AI service (FastAPI) | 146 | 21 |
+| Web (React) | 89 | 12 |
+| Database | 15 | 2 |
+| **Total** | **470** | **52** |
+
+### Breaking changes
+
+None — first stable release.
+
+### Acknowledgements
+
+Built incrementally across 13 phases with a CI quality gate enforcing
+lint, test, build and structure validation on every push.
diff --git a/apps/ai/app/main.py b/apps/ai/app/main.py
index 4a70fb6..bee993d 100644
--- a/apps/ai/app/main.py
+++ b/apps/ai/app/main.py
@@ -8,7 +8,7 @@
def create_app() -> FastAPI:
app = FastAPI(
title="DevForge AI Service",
- version="0.1.0",
+ version="1.0.0",
description="Repository ingestion, analysis and retrieval for DevForge.",
)
app.include_router(health.router)
diff --git a/apps/ai/pyproject.toml b/apps/ai/pyproject.toml
index ad9f272..502977e 100644
--- a/apps/ai/pyproject.toml
+++ b/apps/ai/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "devforge-ai"
-version = "0.1.0"
+version = "1.0.0"
description = "DevForge AI service — ingestion, embeddings, RAG and the provider gateway"
readme = "README.md"
requires-python = ">=3.11"
diff --git a/apps/api/package.json b/apps/api/package.json
index ac6e96d..c1ea232 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -1,6 +1,6 @@
{
"name": "@devforge/api",
- "version": "0.1.0",
+ "version": "1.0.0",
"private": true,
"type": "module",
"description": "DevForge API service (Express)",
diff --git a/apps/api/src/app.js b/apps/api/src/app.js
index 7ac9cc0..02f28d6 100644
--- a/apps/api/src/app.js
+++ b/apps/api/src/app.js
@@ -141,7 +141,7 @@ export function createApp({ auth = null, modules = null, realtime = null, metric
app.get('/', (_req, res) => {
res.json({
service: 'devforge-api',
- version: '0.1.0',
+ version: '1.0.0',
health: '/api/v1/health',
});
});
diff --git a/apps/web/package.json b/apps/web/package.json
index 6f726f3..4b4c70e 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@devforge/web",
- "version": "0.1.0",
+ "version": "1.0.0",
"private": true,
"type": "module",
"description": "DevForge web application (React + Vite)",
diff --git a/apps/web/src/app.jsx b/apps/web/src/app.jsx
index 7e92d63..409c3c8 100644
--- a/apps/web/src/app.jsx
+++ b/apps/web/src/app.jsx
@@ -12,12 +12,22 @@ import { ForgotPassword } from './pages/forgot-password.jsx';
import { ResetPassword } from './pages/reset-password.jsx';
import { Projects } from './pages/projects.jsx';
import { ProjectDetail } from './pages/project-detail.jsx';
-import { TaskDetail } from './pages/task-detail.jsx';
-import { Repositories } from './pages/repositories.jsx';
-import { RepositoryDetail } from './pages/repository-detail.jsx';
-import { Chat } from './pages/chat.jsx';
-const Analytics = lazy(() => import('./pages/analytics.jsx'));
+const TaskDetail = lazy(() =>
+ import('./pages/task-detail.jsx').then((m) => ({ default: m.TaskDetail })),
+);
+const Repositories = lazy(() =>
+ import('./pages/repositories.jsx').then((m) => ({ default: m.Repositories })),
+);
+const RepositoryDetail = lazy(() =>
+ import('./pages/repository-detail.jsx').then((m) => ({ default: m.RepositoryDetail })),
+);
+const Chat = lazy(() =>
+ import('./pages/chat.jsx').then((m) => ({ default: m.Chat })),
+);
+const Analytics = lazy(() =>
+ import('./pages/analytics.jsx').then((m) => ({ default: m.Analytics })),
+);
function LazyFallback() {
return
Loading…
;
@@ -43,10 +53,10 @@ export function App() {
} />
} />
} />
- } />
- } />
- } />
- } />
+ }>} />
+ }>} />
+ }>} />
+ }>} />
+
{children}
);
diff --git a/apps/web/src/layouts/app-shell.jsx b/apps/web/src/layouts/app-shell.jsx
index 086cd26..0077159 100644
--- a/apps/web/src/layouts/app-shell.jsx
+++ b/apps/web/src/layouts/app-shell.jsx
@@ -102,6 +102,12 @@ export function AppShell() {
return (
+
+ Skip to main content
+
@@ -239,6 +245,7 @@ export function AppShell() {
-
+
diff --git a/apps/web/src/test/a11y.test.jsx b/apps/web/src/test/a11y.test.jsx
new file mode 100644
index 0000000..06fb427
--- /dev/null
+++ b/apps/web/src/test/a11y.test.jsx
@@ -0,0 +1,87 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { AppShell } from '../layouts/app-shell.jsx';
+
+vi.mock('../services/api.js', () => ({
+ api: {
+ listOrganizations: vi.fn().mockResolvedValue([{ id: 'org-1', name: 'Acme Corp' }]),
+ listNotifications: vi.fn().mockResolvedValue({ items: [], total: 0 }),
+ },
+}));
+
+vi.mock('../services/socket.js', () => ({
+ connectSocket: vi.fn(),
+ disconnectSocket: vi.fn(),
+ onRealtime: vi.fn(() => vi.fn()),
+}));
+
+vi.mock('../stores/ui.js', () => ({
+ useUiStore: vi.fn((selector) => {
+ const state = { theme: 'dark', toggleTheme: vi.fn() };
+ return selector(state);
+ }),
+}));
+
+vi.mock('../stores/auth.js', () => ({
+ useAuthStore: vi.fn((selector) => {
+ const state = { status: 'authenticated', user: { name: 'Tester' }, logout: vi.fn() };
+ return selector(state);
+ }),
+}));
+
+vi.mock('../stores/workspace.js', () => ({
+ useWorkspaceStore: vi.fn((selector) => {
+ const state = { orgId: 'org-1', selectOrg: vi.fn() };
+ return selector(state);
+ }),
+}));
+
+vi.mock('../stores/notifications.js', () => ({
+ useNotificationsStore: Object.assign(
+ vi.fn(() => ({ items: [], total: 0, unread: 0, loading: false, refresh: vi.fn(), markRead: vi.fn(), markAllRead: vi.fn() })),
+ { getState: vi.fn(() => ({ items: [], total: 0, unread: 0 })) },
+ ),
+}));
+
+function renderShell() {
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ return render(
+
+
+
+
+ ,
+ );
+}
+
+describe('AppShell a11y landmarks', () => {
+ it('includes a skip-to-content link targeting the main element', () => {
+ renderShell();
+ const link = screen.getByRole('link', { name: /skip to main content/i });
+ expect(link).toHaveAttribute('href', '#main-content');
+ });
+
+ it('labels the primary navigation landmark', () => {
+ renderShell();
+ const nav = screen.getByRole('navigation', { name: /main navigation/i });
+ expect(nav).toBeInTheDocument();
+ });
+
+ it('labels the theme toggle button', () => {
+ renderShell();
+ expect(screen.getByRole('button', { name: /toggle theme/i })).toBeInTheDocument();
+ });
+
+ it('labels the organization selector', async () => {
+ renderShell();
+ const select = await screen.findByRole('combobox', { name: /organization/i });
+ expect(select).toBeInTheDocument();
+ });
+
+ it('labels the notifications button', () => {
+ renderShell();
+ expect(screen.getByRole('button', { name: /notifications/i })).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/test/app-shell.test.jsx b/apps/web/src/test/app-shell.test.jsx
index 5b339b3..36e898e 100644
--- a/apps/web/src/test/app-shell.test.jsx
+++ b/apps/web/src/test/app-shell.test.jsx
@@ -43,11 +43,12 @@ describe('AppShell', () => {
const user = userEvent.setup();
renderShell();
- const button = screen.getByRole('button', { name: 'Light' });
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ expect(button).toHaveTextContent('Light');
await user.click(button);
expect(document.documentElement.dataset.theme).toBe('light');
expect(window.localStorage.getItem('devforge.theme')).toBe('light');
- expect(screen.getByRole('button', { name: 'Dark' })).toBeInTheDocument();
+ expect(button).toHaveTextContent('Dark');
});
});
diff --git a/apps/web/vite.config.js b/apps/web/vite.config.js
index d8007e4..ce69a73 100644
--- a/apps/web/vite.config.js
+++ b/apps/web/vite.config.js
@@ -8,6 +8,10 @@ export default defineConfig({
server: {
port: 5173,
},
+ build: {
+ cssCodeSplit: true,
+ sourcemap: false,
+ },
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.js'],
diff --git a/docs/deployment/README.md b/docs/deployment/README.md
new file mode 100644
index 0000000..9a5da01
--- /dev/null
+++ b/docs/deployment/README.md
@@ -0,0 +1,109 @@
+# Deploying DevForge
+
+DevForge runs as a five-service Docker Compose stack:
+
+| Service | Port | Description |
+| --- | --- | --- |
+| **nginx** | 8080 (configurable) | Reverse proxy — entry point for the browser |
+| **web** | 80 (internal) | Vite-built React SPA served by nginx |
+| **api** | 4000 (internal) | Express REST API + Socket.io realtime |
+| **ai** | 5001 (internal) | FastAPI Python AI service |
+| **postgres** | 5432 (internal) | pgvector-backed database |
+
+## Prerequisites
+
+- Docker Engine 24+ and Docker Compose v2
+- A GitHub OAuth App (for repository imports, PR webhooks and the docs/approval flow)
+- An AI provider key (OpenAI or Anthropic) **or** a local Ollama instance
+- A domain name with TLS termination (e.g. via a cloud load balancer in front of port 8080)
+
+## Quick start
+
+```bash
+# Clone the repository
+git clone https://github.com/Alaashamel/devforge.git && cd devforge
+
+# Copy and fill in production secrets
+cp .env.example .env.production
+# Edit .env.production — every secret must be set explicitly in production
+
+# Build and start the stack
+docker compose --env-file .env.production up --build -d
+
+# Run database migrations
+docker compose exec api node packages/database/src/migrate.js
+
+# (Optional) Seed demo data
+docker compose exec api node packages/database/src/seed.js
+
+# Open the UI
+open http://localhost:8080
+```
+
+## Production environment
+
+Copy `.env.production.example` and fill in every value. The API refuses to start
+in production when these secrets are left at their dev defaults:
+
+| Variable | Minimum | Notes |
+| --- | --- | --- |
+| `JWT_ACCESS_SECRET` | 32 chars | Access-token signing secret |
+| `GITHUB_ENCRYPTION_KEY` | 16 chars | AES-256-GCM key for encrypted GitHub tokens |
+| `AI_JOB_SECRET` | 16 chars | HMAC secret shared with the AI service |
+| `API_BASE_URL` | public URL | Must not be `localhost` — used for webhooks |
+
+The web container is built with `VITE_API_URL=/api/v1` (relative), so it
+talks to the API through the same nginx origin. No cross-origin configuration
+is needed.
+
+## Health checks
+
+Every container exposes a health endpoint:
+
+- **api** — `GET /api/v1/health/ready` (200 = ok, 503 = degraded)
+- **ai** — `GET /health`
+- **nginx** — TCP port 80 (Docker default)
+
+Docker Compose wires these as `healthcheck` directives and gates dependent
+services on `service_healthy`.
+
+## Monitoring
+
+- `GET /metrics` (api) — Prometheus text exposition format (counters, histograms, process gauges).
+- Structured JSON logging via pino (`LOG_LEVEL` env var).
+
+Point a Prometheus instance at `http://api:4000/metrics` and scrape on a
+30-second interval.
+
+## TLS termination
+
+Nginx listens on plain HTTP. In production, place a TLS-terminating reverse
+proxy (cloud load balancer, Caddy, traefik, or an additional nginx with
+certificates) in front of port 8080. The API sets `trust proxy 1` and reads
+`X-Forwarded-Proto` for secure cookie decisions.
+
+## Backups
+
+```bash
+# Dump the database
+docker compose exec postgres pg_dump -U devforge devforge > backup.sql
+
+# Restore
+cat backup.sql | docker compose exec -T postgres psql -U devforge devforge
+```
+
+## Scaling
+
+The API and AI service are stateless horizontally (sessions are JWT-based;
+realtime uses Socket.io in-memory presence). For multi-instance deployments
+behind a load balancer, swap in the Socket.io Redis adapter by setting
+`REDIS_URL` and adding a Redis service to the compose file.
+
+## Troubleshooting
+
+| Symptom | Fix |
+| --- | --- |
+| API exits immediately on startup | Check that all production secrets are set in `.env.production` |
+| AI service 502 from API | Verify `AI_SERVICE_URL` and `AI_JOB_SECRET` match between API and AI containers |
+| Socket.io connection refused | Ensure nginx `/socket.io/` location has websocket upgrade headers |
+| Database connection refused | Check `postgres` container is healthy (`docker compose ps`) |
diff --git a/env.production.example b/env.production.example
new file mode 100644
index 0000000..61b303b
--- /dev/null
+++ b/env.production.example
@@ -0,0 +1,41 @@
+# ------------------------------------------------------------------
+# DevForge production environment.
+# Copy to .env.production and fill in every value. The API refuses
+# to start when secrets are left at their dev defaults.
+# ------------------------------------------------------------------
+
+# --- API ----------------------------------------------------------
+NODE_ENV=production
+HOST=0.0.0.0
+PORT=4000
+LOG_LEVEL=info
+
+DATABASE_URL=postgres://devforge:CHANGE_ME@postgres:5432/devforge
+CORS_ORIGINS=https://app.example.com
+
+JWT_ACCESS_SECRET=
+JWT_ACCESS_TTL=15m
+REFRESH_TTL_DAYS=7
+WEB_BASE_URL=https://app.example.com
+
+ARGON2_MEMORY_COST=65536
+ARGON2_TIME_COST=3
+ARGON2_PARALLELISM=1
+
+# --- GitHub -------------------------------------------------------
+API_BASE_URL=https://api.example.com
+GITHUB_CLIENT_ID=
+GITHUB_CLIENT_SECRET=
+GITHUB_OAUTH_CALLBACK_URL=https://api.example.com/api/v1/github/oauth/callback
+GITHUB_ENCRYPTION_KEY=
+
+# --- AI service ---------------------------------------------------
+AI_JOB_SECRET=
+AI_JOB_TTL_SECONDS=300
+AI_ARCHIVE_TTL_SECONDS=900
+AI_PRIMARY_PROVIDER=openai
+AI_OPENAI_API_KEY=
+# AI_ANTHROPIC_API_KEY=
+
+# --- Edge proxy ---------------------------------------------------
+HTTP_PORT=8080
diff --git a/package.json b/package.json
index d783a03..e43ba9b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "devforge",
- "version": "0.1.0",
+ "version": "1.0.0",
"private": true,
"description": "AI-powered developer platform and engineering workspace",
"license": "MIT",