CiviPlan
Archived 1 October 2024 · Next.js · TypeScript · Prisma · PostgreSQL
Overview
CiviPlan was built for a real industry client as part of a structured, client-facing coursework project. The client set the requirements; my team owned how we actually built it, checked against their approval every couple of weeks. I was the team’s technical lead. It’s a web app for creating construction-site and traffic-management diagrams: users select a real-world location through Google Maps autocomplete, attach a satellite image or custom icon, and lay out shapes and signage on a canvas editor. The app ships with 200 standardized construction and traffic safety signage icons, and finished diagrams export to PNG.
Architecture
The frontend and backend were both implemented within the same Next.js app, communicating with Postgres through an ORM. A project record stored its entire canvas state as a serialized JSON blob, alongside location data and an optional image reference. Every request independently verified the requesting user and re-checked project ownership before performing any action on that project.
A small feature-flag system let features - undo/redo, the polygon tool, object rotation - build incrementally while controlling exactly what the client saw during a review.
Building the canvas editor without a graphics library was a project requirement, split mainly between me and one other teammate. It was a small object-oriented layer under React - an abstract shape class and abstract tool class, with concrete subclasses per shape and per tool, wired directly to canvas mouse events. The canvas model was inherently imperative, which didn’t map cleanly onto React’s declarative rendering; tool and style state ended up kept outside normal React state entirely to avoid stale closures in the event handlers.
Design Decisions
The first version of undo/redo pushed a new history entry on every state change, with no debounce. Dragging a single shape for half a second could fill the entire stack, and entries that were functionally identical to the one before them were taking up space right alongside real changes, which made undo and redo feel clunky rather than useful. The version that shipped serialized each shape into a plain object for cheap comparison, so a history entry was only pushed when something had actually changed, and a flag toggled at the start and end of a drag meant a single drag committed one entry on release instead of dozens of intermediate ones.
Polygon editing supported inserting a new vertex by dragging a ghost handle, computed as the midpoint between two existing corners, as well as deleting a vertex by right-clicking it. Neither was obvious from the base shape - there was no intuitive way to add a point to an existing polygon otherwise, and the app was going to be used by people with no technical background, on both sides of the project, so it had to be discoverable without anyone explaining it first.
Challenges
The backend was originally a separate Flask service against a different database, used for early demos rather than anything the client saw in production. It had real security gaps - CORS open to any origin while still accepting an Authorization header, debug mode left on, and an attempt at obscuring the server header that didn’t actually fix anything - so partway through development I moved it into the Next.js app with an ORM, keeping the whole stack in one language and retiring the Flask service entirely.
Before committing to the React implementation, I built a plain JavaScript prototype to validate the geocoding, screenshot, and export workflow. It confirmed the approach was viable before we invested in the full editor, but none of that code survived into the final application.
The client gave us requirements, not a task breakdown. As technical lead, I broke those into tasks people could pick up rather than owning permanently, so no single person’s availability blocked the rest of the team. Team members were at different points in their React experience, so I wrote a set of internal documentation pages covering component patterns and the project’s color and typography tokens, both to set conventions the whole team could follow and to give newer teammates a real way to contribute.
Lessons Learned
Building against a real client’s requirements, with their approval checked every couple of weeks, was different from anything I’d built for myself before - there was always someone outside the team who had to actually sign off on what we shipped, not just be satisfied by it internally. That shaped decisions here that wouldn’t have mattered on a solo project: the feature flags, and the documentation written so people could contribute outside their own area.
Moving off Flask partway through was worth the disruption. If I were starting over, I’d make that call at the beginning instead of waiting until running two stacks became a visible liability.
The clearest lesson from the canvas editor was deciding which layer owns the truth - live state, undo history, persisted data - before trying to sync everything else against it. Undo/redo only worked once that was settled explicitly.
What’s Next
This project is complete and is not under active development. If revisited, the priorities would be a more queryable canvas data model instead of one JSON blob per project - so diagrams could be searched or audited across a client’s projects, and export formats beyond PNG wouldn’t be constrained by how the state is stored.