Tool2025 · Essen

Workshop Report: Vereinswerk

Broadcast associations manage committees, productions and contracts, not members. Lacking software, a custom one was built. Lessons learned, and why the version history was re-rooted.

Vereinswerk Hero

What a media association actually manages

There is plenty of association management software out there. It manages members, dues, and invitations to the annual general meeting. An association whose purpose is production needs something else: Who is assigned where on shooting day, which version of the script is valid, who signed the contract, and which committee approved it.

This resulted in thirteen interconnected areas. The Scheduling plans people against dates. The Script is edited by multiple people simultaneously and retains every version. Contracts are sent out for signature and come back signed. Plus contacts, documents with confidentiality levels, a wiki, a password vault, email templates, and a permissions system that distinguishes between the board of directors, board assistants, office team, and members.

None of this is difficult on its own. The difficulty lies in the fact that it all has to fit together: A contract is tied to a production, the production to a schedule, the schedule to people, who are also contacts.

Where it got interesting

Collaborating on the script

Multiple people edit the same script at the same time. The first version solved this with Cloudflare Durable Objects — one object per document to hold the state. It worked, but it still didn't hold up: The rest of the application ran elsewhere, and for every change, two runtime environments had to align.

It was replaced by a Node sidecar within the same process, using an identical protocol. A single port serves everything, the state resides directly in the database instead of an external service, and the application can be started completely locally. This step cost capabilities but brought cohesion — for a project meant to be maintained for years to come, that is the better trade-off.

A database for every visitor

For the public demo, the usual question arose: How do you show an application where anyone can change everything without the second visitor finding the traces of the first? The usual way — a nightly reset — means that the demo gradually degrades during the day.

Instead, each visitor gets their own database, copied from a seeded template:

CREATE DATABASE "templated_database" TEMPLATE "seed_database"

Postgres physically copies the template. The environment lives for 48 hours after the last use, up to a maximum of seven days, and can be shared with colleagues via an eight-digit code or QR code so they can continue working in the same state. A cleanup job removes whatever has expired. Thus, the separation is not within the application, but one layer below it: Two visitors cannot see each other because they are not in the same database.

What proved effective when working with AI

One thing found more bugs than anything else: never believing a test before seeing it fail. Several times, an audit report stated that a property was secured — but when the safeguard was tentatively removed, everything stayed green. The test was checking something adjacent.

An example from the project: A test was supposed to ensure that the application pins down a token's signature algorithm. The obvious test setup — rewriting the header to one algorithm while actually signing with the other — differentiates nothing: The library calculates using the algorithm from the header anyway and rejects it either way. Only a different setup fails when the pinning is actually missing. Without this cross-check, a test would have sat there for years guarding nothing.

A test you haven't seen fail is not a test. It's an assertion with indentation.

The second thing: Specification first, then tests, then code. Not out of dogma, but because an AI will plausibly fill any gap in the description. Whatever is not written down gets invented — and invented assumptions are later more expensive to track down than any question left open.

The history that looked deleted

The project was meant to become publicly demonstrable. The client remains unnamed, their data remains theirs. So: Names out, sample data in, replace the branding. The directory was clean afterwards. Every file that shouldn't have been there was deleted.

But in Git, deleted doesn't mean gone. A file that was in one commit and removed in the next still resides in the first. The root commit of this repository contained 558 subsequently deleted files.

All of that would have been back with the first git clone.

The fix consisted of two steps. First, the remaining identifiers were replaced across all commits, then the repository was given a new root: A later, clean commit was declared the first, and everything before it was cut off.

git filter-repo --force --replace-text … # replace identifiers
git replace --graft 0d7bc96              # set new root

Since then, the history of the project begins on the day it became presentable. What came before no longer exists — not even in an old object that a tool might eventually dig up again.

After that: five layers instead of an intention

A one-time cleanup lasts exactly until the next careless commit. That is why a chain of five checks now stands guard, each catching something the others can't see: .gitignore for known items, custom patterns for project-specific terms, a tool with around 170 maintained provider rules, an AI review for things patterns cannot know — and a run on a remote server, out of personal reach, because the first four can be disabled on the local machine with a single switch.

The commit message is also checked. None of the other layers read it, and a client name typed into git commit -m ends up in the history just as irrevocably as a file.

The list of terms that must not appear — company name, product names, internal abbreviations — is deliberately not kept in the repository. Otherwise, the very project that isn't supposed to mention the names would contain a sorted list of exactly those names.

The mechanism is versioned, the terms are not.

What remains is a repository where █████████ does not appear, ███████████████ do not appear, and yet the demo shows real workflows — with sample data that looks like actual operations and belongs to no one.

Takeaways

  • Niche software rarely fails because of technology and almost always because the client's terminology isn't mapped out properly. An association that produces doesn't have "projects" — it has productions, schedules, and committees.
  • Building with AI shifts the effort from implementation to description and verification. Whoever doesn't shift along with it quickly gets a lot of code that merely looks plausible.
  • A check you have never seen fail is not a check. This applies equally to tests, hooks, and security guarantees.
  • Anonymization is not a state, but a continuous check. The difference becomes apparent on the commit right after the last cleanup.
↑