Case Study · AI-Assisted Software Delivery

Building an Education Marketplace with Private AI Code Generation

How a two-sided tutoring platform for teachers and students was delivered with a PRD-first method, bulk code generation running entirely on private infrastructure (OpenCode + Ollama), and manual-first debugging assisted by Claude Code and OpenAI Codex.

Client name withheld on request

01At a Glance

~65%
of first-draft code generated by local, private AI
0 lines
of proprietary code sent to public AI APIs during generation
40+
scenario walkthroughs before any code was written
12
PRD chapters, one per product capability
6
product actors modeled in depth

Figures are representative of the engagement and rounded for confidentiality.

02The Project

The client operates an education marketplace connecting verified teachers with students, built for affordability and low-bandwidth access. Students search for teachers by subject, board, language, budget, and rating; browse their notes and courses; and book one-to-one live sessions or small batch classes that work on entry-level phones. Teachers manage availability, publish content, and receive fast payouts. The platform handles identity verification, OTP login, payments and subscriptions, doubt chat, and session recordings.

The engineering difficulty was not any single feature. It was that a two-sided marketplace multiplies scenarios. Every capability behaves differently depending on who is acting, what state they are in, and what can go wrong around them.

03The Technology Stack

We deliberately chose a mainstream, well-documented stack. This was a product decision and an AI decision at the same time: open-weight code models are strongest on technologies with deep public documentation and abundant examples, so a boring stack directly improved the quality of generated code.

Angular TypeScript SPA lazy-loaded modules light on low-end phones REST / JSON FastAPI async Python backend Pydantic models auto OpenAPI docs SQL PostgreSQL system of record transactions and constraints versioned migrations OpenAPI spec + schema feed the context packets Typed at both ends: TypeScript + Pydantic catch bad generated code early.
Figure 2 · The application stack: an Angular SPA talking to a FastAPI backend over REST, with PostgreSQL as the system of record. The OpenAPI spec and migration files fed straight into the generation packets.
  • Frontend: Angular. The application was built on the Angular framework as a TypeScript single-page application. Strict typing, a strong CLI, and an opinionated project structure kept generated components uniform, and lazy-loaded feature modules kept the initial payload small for students on entry-level phones and slow networks.
  • Backend: FastAPI. The backend was built on FastAPI with async Python. Pydantic models gave every request and response a machine-readable schema, and FastAPI’s auto-generated OpenAPI specification became a first-class input to our context packets: the generator always knew the exact shape of the API it was writing against.
  • Database: PostgreSQL. PostgreSQL was our system of record. Bookings, wallet movements, and payouts are exactly the kind of data that wants real transactions, foreign keys, and constraints; schema changes flowed through versioned migrations, and the schema fragments in each context packet came straight from the migration files.
  • End-to-end testing: Playwright. Playwright drove browser-based end-to-end tests against real workflows on the dev environment. Key PRD scenarios, such as searching for a teacher, booking a first free session, and completing a payment, ran as scripted user journeys on every deployment, including mobile viewports and throttled-network profiles that mirror our target users. Because these tests exercised the same flows the PRD described, they were the final word on whether a generated feature actually behaved as specified.

The stack paid a hidden dividend for AI generation: with TypeScript on one end and Pydantic on the other, both edges of every feature were statically typed. A large share of hallucinated or subtly wrong generated code failed the compiler or the schema check before it ever reached a human reviewer.

One more piece completed the picture: environments. Alongside production, a dedicated development deployment ran on its own subdomain. Every accepted, CI-green feature landed there first, where the Playwright suites ran against real workflows and the team exercised it on real phones and throttled networks before it was promoted to production. For a codebase with a high share of generated code, that standing dev environment was not a luxury; it was where generated features earned the right to ship.

04The Real Foundation: Actors, Scenarios, and the PRD

The most important decision in this project had nothing to do with AI. Before a line of code, the early weeks went into a deep product requirement document built around actors and scenario walkthroughs.

Student searches teachers, books 1:1 sessions, reads notes, asks doubts Teacher gets verified, publishes notes & courses, sets slots, earns payouts Guardian pays for the student, receives receipts, monitors progress Platform Ops verification queue, disputes & refunds, content moderation Payment Provider collections, settlements, webhooks that arrive late or twice Session Infra low-bandwidth video, chat & recordings, can fail mid-scenario Each actor got its own goals, screens, failure paths, and clean-up duties in the PRD.
Figure 1 · The six product actors. Treating the payment provider and the session infrastructure as actors, each with its own failure modes, surfaced scenarios that feature lists miss.

Scenario walkthroughs

For every capability, the team ran structured walkthroughs from each actor’s seat: what is this actor trying to do, what do they see when it works, what are all the ways it can go wrong, and who cleans up the state left behind. “Book a one-to-one session” alone produced walkthroughs for withdrawn slots, late payment webhooks, free-session abuse via duplicate accounts, mid-session drops on 3G networks, and guardian-pays-student-attends splits of receipts and reports.

More than forty walkthroughs became a 12-chapter PRD: actor goals, happy path, enumerated failure paths, state diagrams, and acceptance criteria per chapter.

The central lesson of this case study: code generation was never the hard part. Generated code was only ever as good as the PRD slice behind it. Understanding the product deeply, and writing that understanding down in a form a model (or a human) can act on, is where the real leverage lives.

05Private AI Code Generation: OpenCode + Ollama

The client asked that source code and product documents never leave infrastructure they control during bulk generation. So the generation stack was fully local: Ollama served pinned, open-weight code models from a shared Apple Silicon Mac inside the private network, and OpenCode, an open-source agentic coding tool, acted as the harness that read task files, navigated the repository, called the shared Ollama endpoint over the LAN, and applied edits. A model upgrade was treated like a dependency upgrade: pinned by version, regression-tested before adoption.

The models used

The team standardized on a small, boring set of open-weight models served through Ollama, each mapped to a job. All ran as quantized builds (Q4/Q5 class) that fit comfortably in the Mac’s unified memory.

Qwen2.5-Coder (14B / 32B)

Primary code generation from context packets. The 32B variant handled cross-file tasks; the 14B handled single-file tasks.

DeepSeek-Coder-V2 (16B)

Second opinion in the self-review pass. Strong at spotting mismatches with acceptance criteria.

Codestral (22B)

Test skeletons, fixtures, and fill-in-the-middle completions inside existing files.

Llama 3.1 (8B)

Lightweight utility work: commit message drafts, PRD slice summarization, packet formatting.

PRIVATE NETWORK: nothing leaves during generation PRD task slices + context packets schema · conventions exemplar · acceptance OpenCode agentic harness: reads repo, edits files Ollama · shared Mac Apple Silicon, unified memory HTTP API exposed over LAN pinned open-weight models low temperature · batch runs Git repository feature branches CI pipeline lint · unit · integration Human review gate every diff read by an engineer; rejections become rework prompts appended to the context packet, the packet gets smarter over time prompt draft code + tests green build → review rework prompts feed back into the packets
Figure 3 · The private generation rig. OpenCode on each developer machine drives pinned open-weight models served by Ollama from one shared Mac on the LAN; every diff passes a human gate, and every rejection improves the packet.

The pipeline, step by step

1 · Slice the PRDone bounded task:endpoint / screen / FSM 2 · Context packetscenario + schema +conventions + exemplar 3 · GenerateOpenCode → Ollama,code + test skeletons 4 · Self-reviewsecond local pass vsacceptance criteria 5 · Human gateaccept, or send backas a rework prompt 6 · Merge via CIlint, unit, integrationon seeded data rejected diffs return as corrections appended to the packet
Figure 4 · The six-step generation loop. The packet, not the model’s imagination, is the source of truth.

Where local models were reliably good

  • CRUD endpoints and repositories from schema
  • State-machine skeletons from PRD state tables
  • Form screens from field lists; serializers; validation
  • Test scaffolding and fixtures

Where humans stayed in charge

  • Cross-actor flows (booking + payment + notification)
  • Concurrency around slot locking; webhook idempotency
  • Low-bandwidth video edge cases
  • Anywhere two PRD chapters interacted

That split is exactly why the PRD mattered: packets made the first column near-automatic, and the walkthroughs meant the second column was known in advance rather than discovered in production.

06Challenges and the Solutions Adopted

No project like this goes smoothly end to end, and ours was no exception. We faced every challenge described below. We document them here because they shaped the decisions in this case study, and because most teams attempting private AI code generation will meet the same ones.

Challenge 1 · Hardware limitation: no GPU farm, one capable Mac

The problem. The budget did not stretch to a dedicated GPU server or cloud GPU rentals, and most team laptops could not run a serious code model locally. Early attempts to run models on individual machines produced slow, inconsistent results and duplicated model downloads everywhere.

The solution adopted. Ollama ran on a single high-memory Apple Silicon Mac, and its HTTP API was exposed as a web service over the office network: bound to the LAN interface, placed behind a lightweight reverse proxy with token authentication, with every team member’s OpenCode pointed at the same shared endpoint. One machine, one set of pinned models, many consumers. The Mac’s unified memory made mid-size quantized models (14B to 32B) practical without a discrete GPU farm; a simple request queue smoothed peak-hour spikes, and long batch jobs ran overnight.

Dev 1 · OpenCode Dev 2 · OpenCode Dev 3 · OpenCode HTTP over LAN · token auth Reverse proxy auth + request queue Apple Silicon Mac Ollama server · unified memory quantized 14B-32B models Qwen2.5-Coder · DeepSeek-Coder-V2 Codestral · Llama 3.1 models pinned + downloaded once One machine, one set of pinned models, many consumers. Batch generation runs overnight.
Figure 5 · The shared-hardware answer to the GPU problem: one high-memory Mac exposes Ollama as an authenticated web service so the whole team generates against the same pinned models.

Challenge 2 · Live video: build vs buy

The problem. One-to-one tutoring is live video at its core, and self-hosting real-time video infrastructure is deceptively expensive: TURN servers, media servers, bandwidth bills, mobile quirks on low-end devices, and an on-call burden the team did not want.

The solution adopted. The team initially experimented with Jitsi for self-hosted conferencing. It worked as a prototype, but scaling it for low-bandwidth users across regions would have demanded infrastructure effort disproportionate to the product stage. Video conferencing and calls were therefore routed through a third-party provider, saving the cost and effort of building in-house media infrastructure. The integration was isolated behind a thin session-provider interface, so the platform can revisit self-hosting (Jitsi included) later without rewriting booking, recording, or billing logic.

Other challenges we faced on this project

Small context windows on local models

The mid-size open models we ran could not hold the whole repository in mind. Our context packet discipline was the antidote: every task carried exactly the context it needed and nothing more.

Style and convention drift

We saw different models (and different days) produce differently shaped code. The exemplar file in each packet, plus strict linters in CI, kept our generated code looking like one codebase.

Hallucinated APIs

Our local models occasionally invented library methods. The self-review pass caught many; compile checks and tests in CI caught the rest. Nothing hallucinated ever reached review silently.

Model upgrades as regressions

More than once, a “better” model rewrote our working patterns in a new style. We treated model versions like dependency versions, with a regression suite of past packets, and that kept upgrades honest.

Shared-endpoint contention

One Mac serving our whole team meant queuing at peak hours. We scheduled batch generation off-hours and kept interactive tasks small, which preserved a good developer experience.

The temptation to skip the PRD

When generation became cheap, we caught ourselves reaching for the generator before the thinking was done. We enforced a strict rule of “no packet, no generation”, and it paid for itself every time.

Mid-task tool restriction: the Fable case

We also learned that external AI tools can disappear mid-task. One specific task, a review and validation of a PRD chapter, was being run on Fable when it was obstructed and stopped midway due to a restriction imposed by the US government on the use of Fable. The whole task was then reviewed again from the start on OpenAI Codex. The incident cost us a day, and it taught us to keep every task self-contained and portable across assistants, so that no single tool can block the pipeline.

07Code Generation Is One Part of the Ecosystem

A fair accounting of where the effort went. Generation was roughly a seventh of the total, yet it gets all the attention. The honest claim is narrower and more useful: private code generation removed the typing, not the thinking.

30% 15% 15% 15% 15% 10% Product understanding, scenarios, PRD Architecture & data design Code generation (private AI) Review, integration, refactoring Testing & hardening DevOps, release, observability Share of engineering effort (representative) ← the part everyone talks about the part that made it work ↑
Figure 6 · Where the effort actually went. The PRD-and-scenarios share was double the generation share, and it is what made generation effective.

The compounding advantage came from the PRD discipline: the same packets that fed the models also fed reviewers, testers, and every future maintainer.

08Debugging and Error Fixing: Manual First, AI Assisted

Debugging was deliberately kept primarily manual. Generated code that fails teaches you where your packets are weak; outsourcing that lesson back to a model would have hidden the signal.

Reproducefailing PRD scenario Isolatelogs, traces, minimal case Hypothesizeengineer reasons it out Fix by handhuman writes the change Regression testnamed for the scenario Packet updatednext generation avoids it lesson feeds back AI assistants: in the engineer’s hand Claude Code interactive investigation: stack traces, cross-file tracing, candidate fixes, regression tests OpenAI Codex delegated, well-bounded chores: repetitive corrections, type tightening, test variants Hygiene rule: cloud assistants see sanitized, minimal reproductions or non-sensitive slices; bulk work on the private codebase stays local.
Figure 7 · The manual-first debug loop. Assistants accelerate the engineer; they do not replace the engineer’s understanding, and every fix ends as a named regression test tied to a PRD scenario.

09Outcomes

  • The platform shipped with teacher discovery, notes and courses, one-to-one and batch live sessions, verification, and payments, on the timeline the PRD had projected.
  • Roughly two-thirds of first-draft code came from the private generation pipeline, at zero marginal token cost, with zero proprietary code leaving the client’s infrastructure during generation.
  • A single shared Mac running Ollama served the whole team’s generation needs; no GPU farm was ever purchased.
  • The defect pattern validated the method: bugs clustered almost entirely in the cross-actor seams the walkthroughs had flagged as risky, not in the generated single-actor code.
  • The PRD outlived the build: support runbooks, test plans, and onboarding documents all derived from the same actor-and-scenario chapters.

10Lessons

  • Write the PRD as if the reader is a machine, because sometimes it is. Precision about actors, states, and failure paths pays twice: once with the model, once with every human who follows.
  • Keep generation private and boring. Pinned local models, low temperature, one bounded task at a time. Creativity belongs in the scenario room, not in the code generator.
  • Share the hardware, not the chaos. One well-provisioned Mac exposing Ollama as a network service beat a fleet of laptops each running their own models.
  • Buy the commodity, build the differentiator. Third-party video conferencing (after a Jitsi experiment) freed the team to spend its effort on discovery, booking, and trust, which is where the product actually competes.
  • Let humans own the debugger. Manual-first debugging converted every bug into an improvement of the packets, so the pipeline got better with age.
  • Match the tool to the job. Local OpenCode + Ollama for bulk private generation; Claude Code for interactive investigation; Codex for bounded delegated chores; humans for judgment.