Ivo Gabriel

TIC Platform

The problem

In a technical high school track, 100 to 120 students per year have to propose a project idea, argue it out, and end up in working teams. This term two year-groups are running in parallel. That produces 25 to 35 projects a year. The tool for this was Padlet: a wall of sticky notes with no roles and no permissions. Anyone could see any idea the moment it was posted, there was no way to approve or reject before publishing, and building the teams meant cross-referencing sign-ups by hand.

The ideation-and-team-formation cycle runs two weeks. That's the course calendar, not the tool — any solution inherits that constraint, I didn't set it.

What I built, and in what order

The first piece was a moderated idea board: a student posts a card, it sits in pending, invisible to other students until a teacher moves it to approved. A teacher can also send it to waiting — neither rejected nor published, a holding state for "not yet" — or soft-delete it. A student can read their own pending card but can't edit it once submitted, and nobody, not even a moderator, can change who authored a card.

On top of that I added applications: a student applies to a published idea with a role (frontend, backend, design...), not just a like. And on top of applications, a separate screen — /boards/[id]/grupos — where the teacher reconciles applications into final groups: who ended up on which idea, in what role, and whether an idea needs to split into two teams because more students applied than one team can absorb (fork_subgroup_to_card, a case that showed up in production and wasn't in the original design). Closing a group has two levels. A "marked" state that's purely visual, and a "hard lock" that actually blocks adding or removing students at the database level. The first level got used as a stepping stone before committing to the second — the teacher needed a way to signal intent without committing to it yet.

Once the first term was running in production, the loudest signal showed up: groups were formed, but there was no way to see how any given project was actually going. That gap became the second module — a per-group Kanban board with drag and drop, task templates by project type, task dependencies, and calendar/timeline views. It's turned on per board with a toggle, and per group only once that group has a hard lock — the Kanban doesn't exist while team formation is still open.

That's the point the tool stopped being a Padlet replacement and became a three-module platform: ideation, tasks, and a third one — project closeout — that's still a placeholder screen ("Coming soon"). Admitting that in the code, instead of faking it, is also a product call: nothing is worse than a button that promises something that isn't there.

Four idea cards, each with a moderation state band down the left edge, the author below, and counters for likes and applicants.
The idea board. Each card carries its moderation state down the side. This is the demo board: the people and the ideas are fictional.

Build vs. buy

Padlet fell short because it has no roles. There's no way to distinguish "a student sees this" from "a teacher sees this," and without that distinction you can't moderate before publishing or stop a student from editing someone else's idea.

I evaluated Notion before writing a line of code. It fell for two separate reasons. The free plan's limits sit below what the course's real load needs. And paying for the full plan meant subscribing to solve something that still fell outside what Notion is built for: moderating with states and conditioning visibility by role asks for a permissions model, and what Notion offers is a database with views.

The three things I said no to

Automatic team matching.

There's no algorithm that assembles groups. Preselection is manual: the teacher drags applicants from a list into a group (assignDirectly, preselectApplicant). The trade-off is teacher time against teacher judgment. At that age, who works well with whom is known by the person who sees them every week, and no form field captures it.

Live visibility into applications, off by default.

The applicant count is always visible, but who applied to what — name and role — sits behind a per-board toggle the teacher turns on if they want (show_applicants, off by default). The trade-off is peer pressure: watching in real time that "everyone's applying to the popular idea" pushes a student's choice harder than the idea itself does.

AI to draft ideas.

I dropped it for synthetic output — a text generator fills the field, but the idea behind it is still empty. The point of those two weeks is for the student to think through a project, not to produce a paragraph that looks like one.

What real use asked for that wasn't planned

The entire Kanban module. It wasn't in the original plan — that module's requirements doc wasn't written until August, after the ideation cycle had already run start to finish without it. A teacher running 30 projects at once had no way to know which ones were stuck without going and asking each group, one at a time. That's why the teacher view shows every group on the board together, with indicators for overdue tasks and groups with nobody assigned.

The other thing real use asked for was performance. With the board in use, the database started feeling the load: I reworked the RLS policies and consolidated scattered queries into a couple of RPCs (get_board_header, get_card_meta). I applied it in production table by table, with lock_timeout, so it wouldn't block the traffic that was live at that moment. None of that was planned; the load of a term in progress forced it.

A smaller request: exporting the group preselection to CSV. The teacher needed it to bring the sheet into a meeting that happened outside the platform — not everything gets solved by keeping people inside the tool.

The board's stats panel: 154 ideas total, 91 viable, 30 flagged for review.
The moderation panel on a real board: 154 ideas posted, 91 approved as viable, 30 sent back for review.

What didn't work

Comments on ideas — the open feedback thread under each card — never got traction. The debate happened in person, not there. I left them in: pulling them out meant a migration and a product call about what to do with the history already written, and there was no active reason to pay that cost. They're still there, unused. Leaving them cost less than pulling them out, and that's a decision too.

Acknowledged technical debt

Applications and group formation are the most-used modules in the system, and they have the worst test coverage. applications.ts — apply, withdraw an application, check your assigned group — has no tests at all. What does have tests is the "teacher" half of group formation: preselection, subgroup forking, team notes. That's the newest and most complex part of the system (touch-capable drag and drop, a three-state lock, RLS crossing into card_group_closures) running on top of the oldest, least-verified part.

How I built it

I wrote the platform with Claude Code. Around 70% of my commits carry Claude as a co-author, and it sits in the git history for anyone who looks.

What to build and what to drop was my call. The three decisions above came from watching a teacher assemble groups by hand, not from a model's suggestion. One part I reviewed line by line: the RLS policies. A permissions bug doesn't surface in the interface. It surfaces when a student reads something that isn't theirs.

Stack

Next.js 16 (App Router) on React 19. Supabase for Postgres, Auth, and Realtime — all permission logic lives in RLS, not the frontend. Vitest for unit tests on server actions and the Kanban's date/scheduling helpers. @dnd-kit for drag and drop, with real touch support. Deployed on Vercel, CI on GitHub Actions, automatic semantic versioning off Conventional Commits.