cd /news/artificial-intelligence/what-building-a-school-platform-with… · home topics artificial-intelligence article
[ARTICLE · art-91231] src=dev.to ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

What building a school platform without a spec taught me about prompting AI correctly

A developer who built a school management platform using AI without a specification discovered a cross-tenant security vulnerability months later. The bug allowed users to edit exam questions belonging to other schools because the AI-generated code only checked ownership of the exam, not the question itself. The developer fixed it by adding a database check to verify the question belongs to the exam, highlighting the importance of providing detailed specifications to AI.

read5 min views1 publishedAug 10, 2026

TL;DR: I skipped writing a spec, shipped a cross-tenant security bug, and fixed it with one extra database call. Here's what that taught me about why AI builds exactly what you ask for — not what you meant.

I built a school management platform the way most people build their first thing with AI: I opened Gemini Antigravity and asked it to build a school management system. No spec, no plan, just an idea and a prompt. Then another feature. Then another.

It worked. That was the problem. It worked well enough that I kept going for months without stopping to ask what "working" actually meant.

When I finally sat down with Claude to properly assess what I'd built—not add to it, just look at it—I found security vulnerabilities. Performance problems. Scaling issues. Dead code that had been quietly rotting since week two. None of it was because the AI I used was bad at writing code. It was because I never told it what "done" needed to include.

I'm a solo builder, not a senior engineer. I picked up what I know through internships at Flincap and Venturage and by building things myself, including this platform, often the hard way. This is what I learned about the gap between "AI wrote code that works" and "AI wrote code that's actually safe to ship" and why that gap is entirely mine to close, not the AI's.

Here's a concrete one. Somewhere in the platform, there's a route that lets a teacher or admin update exam questions. I asked AI to build "a way to edit CBT questions," a reasonable request, got working code, and moved on to the next feature.

Months later, running a security pass with Claude, I found the route was checking the right thing at the wrong level. It confirmed the person editing the question owned the exam they were working in. What it never checked was whether the specific question being updated actually belonged to that exam. So if you had a valid question ID from anywhere, even a completely different school on the platform, you could authorize through your own exam and then reach out and overwrite a question that wasn't yours.

Nobody exploited this that I know of. But sitting with it after the fact, the failure made complete sense once I understood what actually happened: I never told the AI that questions needed to be scoped to their exam. I told it to build "editing," and it built one level of ownership for editing, checked and working. It didn't independently reason its way to "and also lock this down two levels deeper," because I never asked it to, and it has no way of knowing what I didn't say.

That's the whole lesson in one bug. AI doesn't fail because it's careless. It fails because it's literal: it solves the problem you handed it, not the problem you were picturing when you handed it over. The gap between those two things is exactly where a spec lives, and it's exactly what I skipped.

Fixed now: the route requires the question to belong to the exam it was authorized against before any update happens. Small check, but it only exists because someone, in this case, AI reviewing its own kind of mistake, went looking for it after the fact instead of before it.

Stripped of any real names or paths, the pattern looked roughly like this. Before:

// checks that the caller owns the exam...
const exam = await db.exam.findFirst({
  where: { id: examId, ownerId: userId },
});
if (!exam) throw new ForbiddenError();

// ...then updates a question by raw id, with no check
// that the question actually belongs to that exam
await db.question.update({
  where: { id: questionId },
  data: updates,
});

After:

const exam = await db.exam.findFirst({
  where: { id: examId, ownerId: userId },
});
if (!exam) throw new ForbiddenError();

// the missing link: verify the question belongs to
// the exam we just proved the caller owns
const question = await db.question.findFirst({
  where: { id: questionId, examId: exam.id },
});
if (!question) throw new ForbiddenError();

await db.question.update({
  where: { id: questionId },
  data: updates,
});

One extra findFirst

call. That's the entire fix. The hard part was never writing it; it was knowing to ask for it in the first place.

I used to think the tool mattered most — that if I just found the right AI coding assistant, it would catch what I missed. I've used Codex, Gemini Antigravity, Claude, and (briefly) Warp. The failure mode was the same across every one of them, which is what convinced me this was never a tool problem. Tool choice changes the ceiling — how much a good agentic mode or bigger context window can catch on its own — but it never replaces the floor, which is you actually specifying what needs to be true.

So here's what changed:

I write down the technical requirements before I prompt, not a formal spec doc necessarily, but a real answer to "What does this need to handle?, not just "What does this need to do?" For the CBT question route, that's the difference between "let teachers edit questions" and "let teachers edit questions they own, scoped to exams they own, and reject anything that doesn't match." AI will build exactly the version you describe. It won't build the version you assumed was implied.

I also try to keep the codebase itself readable with decomposed functions, clear naming, and minimal nesting: AI writes better code when it can actually read the code that's already there. A messy existing codebase produces messier suggestions on top of it. Readability isn't just for the next human who touches your code. It's context your AI tool reads every time you prompt within an existing project.

None of this makes me an expert. I still don't know if I'm doing "spec-first prompting" by the book, whatever the book is. But I know what it costs to skip it, because I paid that cost building this platform, and I'd rather pay it in a spec doc than in a production security audit.

What's the smallest spec you write before prompting, or do you skip it too?

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @gemini antigravity 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/what-building-a-scho…] indexed:0 read:5min 2026-08-10 ·