Ligang Yan颜力刚

CurioSeed V4: stack choices and how a small team works with AI

The key decisions behind CurioSeed V4, laid out with their costs: Supabase or Neon, React SPA or Next.js, client-side fetching or SSR, when to split into a monorepo, and a working method of people plus AI plus rule guardrails. Every choice comes with the pitfalls we hit.

engineeringnextjsneonclaude-codeteam

中文版:CurioSeed V4:技术选型与团队协作实践

This is not a showcase. I want to lay out the key decisions behind the past few months of building CurioSeed V4: why we chose what we chose, what it cost, how it scales, and how people and AI work together. Each one comes with the pitfalls we hit. Nothing hidden.

You should leave with three things:

  1. What our current architecture looks like, and which boundaries are deliberate;
  2. The real trade-offs behind three key choices (database, framework, data fetching);
  3. A reusable way of working: people, AI, and rule guardrails.

1. The architecture today

The product: a nurturing game for children aged 5 to 12 in which they “teach an AI pet to get smarter”. Lessons, quizzes, a knowledge galaxy and a shop form one loop.

The request path, top to bottom

Browser (Next.js App Router, "use client" pages + React Query)
        │  fetch /api/*

Route Handlers (src/app/api/**)
   · getSessionUser auth        → 401
   · zod input validation       → 400
   · safeRoute unified errors   → 500


server-only data layer (src/lib/neon.ts)   ← injects the session JWT


Neon Data API (PostgREST) + RLS / atomic RPCs
─────────────────────────────────────────────
Auth: Neon Auth (Better Auth, cookie session)  ← route protection in src/proxy.ts
Deploy: Vercel (iad1, next to Neon us-east-1)

The stack

Area Choice
Framework Next.js 16 (App Router) · React 19 · TypeScript 5
Styling Tailwind v4 · Framer Motion
Data Neon Postgres + Data API · Neon Auth
Client React Query · zod
Testing Vitest + Playwright
Deploy Vercel (iad1, same region as Neon)

Three architectural boundaries

  • The client only touches /api/*; it never talks to the database.
  • Database access lives only in the server-only data layer; importing neon.ts from a component is forbidden.
  • Auth is enforced in proxy.ts as the backstop; unauthenticated requests are stopped at the routing layer.

2. The key choices

2.1 Database: Supabase or Neon

Dimension Supabase Neon (our choice)
Postgres Yes Yes
Auto data API PostgREST Data API (PostgREST-compatible)
Auth Supabase Auth Neon Auth (Better Auth, identities in your own DB)
Killer feature Mature ecosystem Database branching: copy-on-write clones in seconds

Why Neon

  • Serverless plus database branching: every PR can clone a throwaway database to run migrations and tests, then discard it. This is the single most valuable feature for us.
  • Neon Auth and the Data API match Supabase’s equivalents: .from().select(), RLS, auth.user_id(). The mental model migrates almost one to one.
  • Same region as Vercel, serverless-friendly; latency and scaling both behave.

The downsides, honestly

  • @neondatabase/auth and postgrest-js are still beta (0.x) with thin documentation. In several places the only way to understand the API was to read the type definitions.
  • The Data API’s schema cache does not refresh itself: after creating a table you must click “Refresh schema cache” by hand or you get PGRST205. We hit this.
  • The bundled better-auth had a high-severity vulnerability, waiting on upstream.

How we manage the risk

  • Pin exact versions and let Dependabot watch for updates;
  • Run migration regressions on a temporary Neon branch in CI;
  • Write “refresh the schema after DDL” into the rules so the AI remembers for you.

2.2 Framework: React SPA or Next.js

Choice: Next.js 16 (App Router).

Advantages

  • All-in-one: routing, SSR, Route Handlers (the back end), proxy (middleware) and bundling in one package.
  • Front end and back end in one repo, one language, one set of types (src/lib/types.ts is shared), so one less layer of glue.
  • Zero-config deployment on Vercel; Server and Client components as needed.

Disadvantages

  • Next 16 has many breaking changes, and it is newer than the training data: middleware became proxy, caching semantics changed. Both people and AI reach for the old API. Our hedge: AGENTS.md explicitly warns “read node_modules/next/dist/docs/ before writing”.
  • The App Router’s mental load: the Server/Client boundary, server-only markers, and the way “use client” spreads.

Versus a plain React SPA: we skip building our own routing, SSR, API and build pipeline, at the cost of being bound to Next’s conventions. For a small team that needs to ship a full-stack product fast, that trade is worth it.

2.3 Data fetching: client-side or SSR

This is the decision we genuinely agonised over and switched on.

Final choice: separation. Client-side React Query hooks → /api/* Route Handlers → the data layer.

Why not fetch in Server Components

  • The product is highly interactive; most pages are “use client”.
  • Neon Auth uses cookies and a server-side session; mixing that with client-managed tokens causes trouble.
  • We wanted a clean boundary (server-only neon.ts never reaches the client) and a REST contract ready for future clients such as a mobile app.

Advantages: clear boundaries that are easy to test (mock /api); caching, retries and invalidation handled uniformly by React Query; database access only on the server, which is safer.

Disadvantages: more boilerplate than pure SSR (every resource needs an endpoint and a hook); no direct first paint, so there is a loading state; it needs discipline, no raw fetch in components, no importing neon.ts.

A real pitfall: under concurrent queries auth.token() raced, some requests lost their token and were blanked by RLS. The fix: request-level deduplication with React cache(), fetching the token once and reusing it.

The compromise: aggregate endpoints (/api/me returns profile, pet, quests and test in one call) cut round trips. Pure display and SEO pages can still use Server Components later.

3. Scaling: a pnpm monorepo

Today: a single Next app, front and back end in one repo, Route Handlers as the back end. Fastest for the MVP stage.

Once users arrive, split into a monorepo with pnpm workspaces:

apps/
  web/        Next.js front end
  api/        standalone back end (heavy logic moved out of Route Handlers)
  worker/     background jobs: AI conversations, aggregation, scheduled tasks
packages/
  ui/         shared components
  types/      shared domain types (today's src/lib/types.ts)
  config/     eslint / tsconfig / tailwind presets
  core/       pure business logic (quizBank / shopCatalog / rng …)

Why pnpm: hard links save disk, strict dependencies prevent phantom deps, the workspace protocol, and it is faster than npm and yarn. Turborepo on top for task orchestration and caching.

When to split: when the back end gets heavy (LLM conversations, real-time, scheduled jobs) and needs to scale and deploy independently; when the team grows and packages need owners by domain. No over-engineering now. A single repo stays a single repo until users or headcount force the split.

4. Team workflow: Git and issues

Issues for requirements and bugs: requirements, bugs and technical debt all go through GitHub Issues, graded high / medium / low, tagged with their source, with a suggested fix attached. Findings from AI review are triaged into issues too.

Branches and PRs, no forks: the team branches directly in the main repo. The iron rule is to always start from the latest origin/main:

git fetch
git checkout -b <type>/<topic> origin/main

Names follow feat/*, fix/*, chore/*; commits use the conventional style. Everyone can open PRs; green CI plus passing tests means it can merge. A merged PR ends the branch; further changes get a new branch and a new PR.

Quality gates (CI)

Gate What
Lint Hard block; no merge without it
next build Type check
npm test Full Vitest run
DB branch regression Every PR runs migrations plus assertions on a temporary Neon branch

Code review: people plus automated AI review (Gemini). Real problems become issues; false positives get labelled.

5. Working with AI: Codex / Claude plus CLAUDE.md and rules

The loop

Person (outline, decisions)
  → AI (Claude / Codex)   implement, read docs, write tests, open the PR
  → AI (Gemini)           review
  → Person                approve and merge

Three layers of markdown governance, tool-agnostic and plain text:

File Purpose
CLAUDE.md (repo root) The project charter: product, stack, commands, structure, current state, global conventions, Git rules. The AI reads it first.
AGENTS.md Hard warnings for the AI, such as “Next 16 has breaking changes; read the official docs first, don’t apply the old API”.
.claude/rules/* Detailed rules loaded automatically per directory (nextjs, styling, neon, testing). Edit an area and its rules load with it.

Rules are guardrails: every pitfall becomes a convention. A few examples:

  • The client never imports the server-only neon.ts;
  • Every new RPC is SECURITY DEFINER;
  • Decorative randomness uses seeded(), never Math.random;
  • After any DDL change, refresh the Data API cache.

Hit a pitfall once, write it into the rules, and neither people nor AI hit it again. The core value is turning experience into executable context. Codex and Claude read the same conventions, so switching tools loses no knowledge.

6. Testing

Three layers plus CI gates

Layer Tool What it tests
Unit Vitest Pure functions (quizBank, shopCatalog)
Component Vitest + RTL Props to render (jsdom)
Integration Vitest (node) Route Handlers with neon and guards mocked; asserts 401 / 400 / 500 and response shape, no real database
E2E Playwright Smoke: unauthenticated redirect, sign-in renders, /api returns 401 (no real Neon)
DB regression CI + Neon branch Per-PR clone, run migrations, structural assertions (tables, RLS, functions, seeds)

Key conventions: server-only is aliased to a no-op in Vitest so server modules are testable; real RLS and auth end-to-end stay as gated e2e that needs credentials and does not run in default CI; lint, build and test must all pass before merging to main.

7. Summary and next steps

Three principles

  1. Choosing tools: trade “new infrastructure that fills the capability gap (Neon)” for developer experience and room to evolve; control beta risk with pinning, migration CI, Dependabot and rule guardrails.
  2. Architecture: client/server separation, server-only boundaries, no swallowed errors, validated inputs. Lay the foundation (stability, observability, tests) before stacking features.
  3. Collaboration: lightweight Git (branches and PRs, anyone can merge, tests as the gate) multiplied by AI guardrails (CLAUDE.md and rules that freeze experience into conventions).

Next: on features, wire the AI pet’s conversation to an LLM and give the parent page real statistics; on hardening, atomic settlement, rate limiting and Sentry; on scale, split into the pnpm monorepo when the triggers fire.

中文版.