How I built arbitres.ca with Claude and Copilot

Building arbitres.ca with Claude Code and GitHub Copilot

How I built arbitres.ca with Claude and Copilot

Part 2 in the “Behind arbitres.ca” series: Part 1 – Scheduling umpires without Excel | Version française

In the first article, I introduced arbitres.ca and its stack. Here I move on to the method. At the end of July, the repository had 280 commits, 68 specs and 9 skills, built up over about two and a half months of evenings.

I also show the places where the AI got it wrong and what caught it.

Deciding what to build before writing code

It is before writing code that AI has been most useful to me.

I use the grill-me skill I talked about in the series on skills. It asks questions one at a time until the decisions are made. I used it from the start of the project, and I come back to it every time I have a big chunk to decide.

A session like that in July settled the product positioning. Rather than redoing the schedule management that other platforms already handle, we decided to put the effort on assignment when umpires are missing. That gave me a design constraint I have kept since: no configuration screen for those features. The thresholds and the weights are hard coded, and the application explains its suggestions instead of offering settings.

The same session also led to stopping a spec. I had written one for automatically importing files from an external platform. The column format changes from one association to another and is not publicly documented. I put the spec on hold with a condition for picking it back up: having a real export file in hand. Without that, I was guessing.

One spec per feature

Every feature starts with a file in specs/. The template is encoded in a /new-spec skill: context and design decisions, pages with their access roles, database schema with the DDL and the RLS policies, business logic, C# models, services, i18n, what is out of scope, and acceptance criteria to check off.

The “out of scope” section is the one that helped me most. It gives an official place to the good ideas that would derail delivery.

The acceptance criteria close the spec. Here is an excerpt from the shortage alert one, translated from the French original:

- [x] The `v_penurie` view returns one row per (game, position) at risk across the 3 positions
- [x] The horizon is 14 days, hard coded, no parameter and no configuration column
- [x] No new configuration screen
- [x] Schedule conflicts are ignored in the calculation (no call to `arbitre_a_conflit`)
- [x] A filled position (active assignment) never shows up in the view

Each line is checked one at a time. That is what I tick before closing the spec, and it is also what the agent rereads when it comes back to the subject weeks later.

Three other files serve as reference. 00-prioritization.md is the roadmap with the status of every spec. CLAUDE.md acts as the project memory: the stack, the conventions, the known pitfalls. schema_complet.sql documents the final state of the schema, synchronized with every migration.

The agent does not remember yesterday’s conversation. It reads the spec and CLAUDE.md. That is what lets me pick a feature back up three weeks later without explaining everything again.

Claude writes, Copilot reviews

The work happens on branches, with Claude Code implementing from the spec. Then I open a pull request, and Copilot reviews it.

Having one agent review another agent’s code works better than asking the same one to reread itself. The repository numbers show it: 171 commits signed by Claude, 91 by me, and 18 by the Copilot agent pushing its own fixes.

That review caught real problems. The most serious one involved the public registration form. It had neither rate limiting nor bot protection, and the write path was reachable without authentication. Anyone could flood the requests table at no cost to them. Copilot raised it during a PR review. I logged the item in specs/tech-debt.md instead of fixing it right away, and it was fixed later with an Edge Function that centralizes the honeypot, the per IP limit and the captcha validation.

That habit is as useful as the review. A problem found during a PR about another subject should not derail the PR in progress. It goes into the technical debt file with its source, its risk and the planned fix.

From guardrails to skills that call each other

The repository skills started out as guardrails. /new-page knows the design system, the bilingual resource files to create and the project’s pitfalls. /schema-sync applies a Supabase migration and synchronizes the documentation. /maj-tech handles the periodic dependency updates.

Those skills mostly contain the mistakes I already made. Two of them come up often with the Supabase C# client.

The first one: every public computed property on a model gets serialized into the request body, which triggers a PGRST204 error about a column that does not exist.

// included in the JSON sent to Supabase, so PGRST204
public bool EstGlobal => TerrainGlobalId.HasValue;

// excluded from serialization
[JsonIgnore]
public bool EstGlobal => TerrainGlobalId.HasValue;

The second one cost me an evening. Almost every model in the project has a primary key generated by the database, and the attribute is written [PrimaryKey("id", false)] to omit it on insert. When I added a table whose key is supplied by the client, the AI reused the dominant pattern of the repository. The column was therefore missing from the request and the insert failed with no visible error. Umpire preferences simply would not save.

// key generated by the database
[PrimaryKey("id", false)]

// key supplied by the client, it has to be included
[PrimaryKey("arbitre_id", true)]

This is the kind of bug AI produces easily. The code followed the repository convention, which is usually fine, but that case was an exception.

Not every encoded rule comes from a bug. The site is prerendered at dotnet publish with BlazorWasmPreRendering.Build, because Blazor WebAssembly otherwise serves an empty HTML shell to search engines. The consequence is easy to break without knowing: on a public page, the content has to be the default render, and redirecting an already connected user has to wait for OnAfterRenderAsync. A page that redirects too early looks perfect in a browser and stays empty for the crawler. Nothing in the code prevents breaking it, so the rule lives in the /new-page skill and in CLAUDE.md.

Since then, the skills have started calling each other. A dev-feature skill chains the spec writing, the migration, the service layers, the pages and the tests. It stops at checkpoints: after the spec so I can confirm it, and if the Supabase security analysis raises a warning that was not expected. Its usage criteria say that an ambiguous spec, or one touching several domains, does not go through it. The stopping points are written into the skill, like the steps.

Review lessons go back into the repository

When a PR review raises a fix that is likely to come back, I write the convention into CLAUDE.md.

One July commit adds two conventions from a single review, about how to materialize a filtered list and about the default value of a DTO field mirroring another table’s status. The same commit adds a session start hook that installs the .NET SDK in web sessions, without blocking if the network prevents the download.

It shows in the numbers. Between July 21 and July 31, Claude’s commits went from 115 to 171 while mine stayed at the same number. The more cases the conventions and the tests cover, the more I can delegate without going back over it.

Pure business logic is in static *Helper classes tested with xUnit. Code buried in a service that calls Supabase is not testable, and without tests I would not delegate as much.

Happy coding, and if you are starting a project with an agent, begin by writing the file that explains your conventions. That is what paid off the most for me.


This post was written with AI assistance and edited by me.


See also