← All posts

· XingAI Meal Coach AI

Meal AI: Decision Ledger in a Next.js App with No Backend

Meal AI v4 is a pure Next.js app — no Python backend, no database, all decision logic in TypeScript on the client. The Decision Ledger schema expects a write call when a recommendation is shown. How do you adopt the cross-product pattern when your product has no backend?

Next.js API routes. Here's how.

The Constraint

Meal AI v4 has no Python worker, no SQLite, no Supabase auth. Its stack:

{
  "dependencies": {
    "next": "16.2.4",
    "react": "^19"
  }
}

No better-sqlite3, no Prisma, no ORM. The decision logic (decision-engine.ts) runs in the browser.

The Solution: Next.js API Route with In-Process Store

// app/api/decisions/route.ts

// In-process store — keyed by session_id → Decision[]
const _store = new Map<string, Decision[]>();

export async function POST(request: NextRequest): Promise<NextResponse> {
  const body = await request.json();
  const decision: Decision = {
    id: `meal_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
    product: "meal-ai",
    domain: body.domain ?? "meal-plan",
    question: body.question,
    recommendation: body.recommendation,
    reasoning: body.reasoning ?? [],
    confidence: body.confidence ?? 0.4,
    action_taken: null,
    outcome: null,
    created_at: new Date().toISOString(),
    // ...
  };
  const rows = _store.get(body.session_id) ?? [];
  rows.push(decision);
  _store.set(body.session_id, rows);
  return NextResponse.json(decision, { status: 201 });
}

Three endpoints: GET (list), POST (record), PATCH (update action_taken). All stateful in the in-process Map.

Wiring into the Page

When the user triggers a meal decision, the frontend sends a fire-and-forget POST:

function runQuickDecision() {
  setShowResult(true);
  // ... scroll ...

  const decision = getTodayDecision(userState);
  fetch("/api/decisions", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      session_id: sessionStorage.getItem("xingai_session") ?? "anon",
      domain: `meal-plan/condition/${decision.state.toLowerCase()}`,
      question: `Meal decision: ${decision.mode} mode, BP=${userState.bloodPressure}`,
      recommendation: decision.rules.join("; "),
      reasoning: decision.reason,
      confidence: decision.confidence,
    }),
  }).catch(() => {/* non-blocking */});
}

.catch(() => {}) — the ledger write is non-blocking. A network error or server cold start does not affect the user's experience.

The Tradeoff: In-Process vs. Persistent

The in-process Map resets on server restart or cold start. This is v1 behavior, explicitly chosen:

ApproachPersistenceComplexityRight for v1?
In-process MapSession-onlyZero✅ Yes
better-sqlite3Across restartsMediumNot yet
SupabaseCross-deviceHighLater

The schema is already correct. When Meal AI gets a backend, swapping _store for a database is a one-file change. The contract (schema shape, endpoint paths, frontend call) stays the same.

What This Proves

Meal AI becomes the second Decision Ledger adopter (ADR-003) — validating that the cross-product schema works for:

  • A Python FastAPI backend (Invest Decision Engine)
  • A Next.js client-side app with an API route (Meal AI)

The pattern is framework-agnostic. The schema is the contract. The implementation adapts to the product's stack.

Related ADR: Meal AI ADR-003: Decision Ledger Adoption