@@ -45,48 +196,24 @@ export default function BuildPage() {
);
}
- const hasRepo = Boolean(project?.giteaRepoUrl);
- const hasPRD = Boolean(project?.prd);
-
- if (!hasPRD) {
+ // No PRD yet
+ if (!prd) {
return (
-
+
-
- ๐
-
-
+
๐
+
Complete your PRD first
- Finish your discovery with Atlas, then the builder unlocks automatically.
+ Finish your discovery conversation with Atlas, then the architect will unlock automatically.
-
+
Continue with Atlas โ
@@ -94,83 +221,255 @@ export default function BuildPage() {
);
}
- if (!hasRepo) {
+ // PRD exists but no architecture yet โ prompt to generate
+ if (!architecture) {
return (
-
-
-
- โก
-
-
- PRD ready โ build coming soon
+
+
+
๐๏ธ
+
+ Ready to architect {architecture ? (architecture as Architecture).productName : "your product"}
-
- The Architect agent will generate your project structure and kick off the build pipeline.
- This feature is in active development.
+
+ The AI will read your PRD and recommend the technical structure โ apps, services, database, and integrations. You'll review it before anything gets built.
+ {error && (
+
+ {error}
+
+ )}
+
+ {generating && (
+
+ This takes about 15โ30 seconds
+
+ )}
);
}
+ // Architecture loaded โ show full review UI
return (
-
+ You can still regenerate or adjust the architecture before scaffolding begins. Nothing has been built yet.
+
+
+ Choose your design โ
+
+
+ ) : (
+
+
+ Does this look right?
+
+
+ Review the structure above. You can regenerate if something's off, or confirm to move to design.
+ You can always come back and adjust before the build starts โ nothing gets scaffolded yet.
+
+
+
+
+
+
+ )}
+
+
+
);
}
diff --git a/app/api/projects/[projectId]/architecture/route.ts b/app/api/projects/[projectId]/architecture/route.ts
new file mode 100644
index 0000000..56c84b2
--- /dev/null
+++ b/app/api/projects/[projectId]/architecture/route.ts
@@ -0,0 +1,212 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/lib/auth/authOptions";
+import { query } from "@/lib/db-postgres";
+
+const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
+
+// ---------------------------------------------------------------------------
+// GET โ return saved architecture (if it exists)
+// ---------------------------------------------------------------------------
+
+export async function GET(
+ _req: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.email) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { projectId } = await params;
+
+ try {
+ const rows = await query<{ data: any }>(
+ `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
+ [projectId]
+ );
+ const data = rows[0]?.data ?? {};
+ return NextResponse.json({
+ architecture: data.architecture ?? null,
+ prd: data.prd ?? null,
+ });
+ } catch {
+ return NextResponse.json({ architecture: null, prd: null });
+ }
+}
+
+// ---------------------------------------------------------------------------
+// POST โ generate architecture recommendation from PRD using AI
+// ---------------------------------------------------------------------------
+
+export async function POST(
+ req: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.email) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { projectId } = await params;
+ const body = await req.json().catch(() => ({}));
+ const forceRegenerate = body.forceRegenerate === true;
+
+ // Load project PRD + phases
+ let prd: string | null = null;
+ let phases: any[] = [];
+
+ try {
+ const rows = await query<{ data: any }>(
+ `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
+ [projectId]
+ );
+ const data = rows[0]?.data ?? {};
+ prd = data.prd ?? null;
+
+ // Return cached architecture if it exists and not forcing regenerate
+ if (data.architecture && !forceRegenerate) {
+ return NextResponse.json({ architecture: data.architecture, cached: true });
+ }
+ } catch {
+ return NextResponse.json({ error: "Project not found" }, { status: 404 });
+ }
+
+ if (!prd) {
+ return NextResponse.json({ error: "No PRD found โ complete discovery first" }, { status: 400 });
+ }
+
+ try {
+ const phaseRows = await query<{ phase: string; title: string; summary: string; data: any }>(
+ `SELECT phase, title, summary, data FROM atlas_phases WHERE project_id = $1 ORDER BY saved_at ASC`,
+ [projectId]
+ );
+ phases = phaseRows;
+ } catch { /* phases optional */ }
+
+ // Build a concise context string from phases
+ const phaseContext = phases.map(p =>
+ `## ${p.title}\n${p.summary}\n${JSON.stringify(p.data, null, 2)}`
+ ).join("\n\n");
+
+ const prompt = `You are a senior software architect. Analyse the following Product Requirements Document and recommend a technical architecture for a Turborepo monorepo.
+
+Return ONLY a valid JSON object (no markdown, no explanation) with this exact structure:
+{
+ "productName": "string",
+ "productType": "string (e.g. PWA Game, SaaS, Marketplace, Internal Tool)",
+ "summary": "2-3 sentence plain-English summary of the recommended architecture",
+ "apps": [
+ {
+ "name": "string (e.g. web, api, simulator)",
+ "type": "string (e.g. Next.js 15, Express API, Node.js service)",
+ "description": "string โ what this app does",
+ "tech": ["string array of key technologies"],
+ "screens": ["string array โ key screens/routes if applicable, else empty"]
+ }
+ ],
+ "packages": [
+ {
+ "name": "string (e.g. db, types, ui)",
+ "description": "string โ what this shared package contains"
+ }
+ ],
+ "infrastructure": [
+ {
+ "name": "string (e.g. PostgreSQL, Redis, Background Jobs)",
+ "reason": "string โ why this is needed based on the PRD"
+ }
+ ],
+ "integrations": [
+ {
+ "name": "string (e.g. Ad Network SDK)",
+ "required": true,
+ "notes": "string"
+ }
+ ],
+ "designSurfaces": ["string array โ e.g. Web App, Mobile PWA, Admin"],
+ "riskNotes": ["string array โ 1-2 key architectural risks from the PRD"]
+}
+
+Be specific to this product. Do not use generic boilerplate โ base your decisions on the PRD content.
+
+--- DISCOVERY PHASES ---
+${phaseContext}
+
+--- PRD ---
+${prd}`;
+
+ try {
+ const res = await fetch(`${AGENT_RUNNER_URL}/atlas/chat`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ message: prompt,
+ session_id: `arch_${projectId}_${Date.now()}`,
+ history: [],
+ is_init: false,
+ tools: [], // no tools needed โ just structured generation
+ }),
+ signal: AbortSignal.timeout(120_000),
+ });
+
+ if (!res.ok) {
+ throw new Error(`Agent runner responded ${res.status}`);
+ }
+
+ const data = await res.json();
+ const raw = data.reply ?? "";
+
+ // Extract JSON from response (strip any accidental markdown)
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
+ if (!jsonMatch) throw new Error("No JSON in response");
+
+ const architecture = JSON.parse(jsonMatch[0]);
+
+ // Persist to project data
+ await query(
+ `UPDATE fs_projects
+ SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{architecture}', $2::jsonb, true),
+ updated_at = NOW()
+ WHERE id = $1`,
+ [projectId, JSON.stringify(architecture)]
+ );
+
+ return NextResponse.json({ architecture, cached: false });
+ } catch (err) {
+ console.error("[architecture] Generation failed:", err);
+ return NextResponse.json(
+ { error: "Architecture generation failed. Please try again." },
+ { status: 500 }
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// PATCH โ confirm architecture (sets architectureConfirmed flag)
+// ---------------------------------------------------------------------------
+
+export async function PATCH(
+ _req: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.email) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { projectId } = await params;
+
+ try {
+ await query(
+ `UPDATE fs_projects
+ SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{architectureConfirmed}', 'true'::jsonb, true),
+ updated_at = NOW()
+ WHERE id = $1`,
+ [projectId]
+ );
+ return NextResponse.json({ confirmed: true });
+ } catch {
+ return NextResponse.json({ error: "Failed to confirm" }, { status: 500 });
+ }
+}