I’m Diana Lopez, a product engineer and the founder of PixelsWithin, a software studio where I design and build websites and apps for businesses. When a system is genuinely ambiguous—when there’s no existing pattern to copy and no spec to follow—I don’t start by sketching screens or writing code. I start by asking what the actual objects are, and how they relate to each other.
If you’re handing a project to a developer with no documentation, no clear spec, and nothing to point to except “here’s roughly what it needs to do”—this is the part that usually worries people. Will they freeze without a spec to follow? Will they start guessing and build something that doesn’t hold together? This is what I actually do instead.
Here’s what that looked like on a real project.
The problem underneath the problem
I built Cute Magick, a self-hosted website runtime and manager, because of a gap I kept running into: people who don’t yet have the skill or infrastructure to set up their own safety net end up with none at all. Someone experienced can wire up staging, rollback, and version control for themselves. Someone who’s just learning to build usually can’t—not because they’re on their own instead of a team, but because that safety net takes real know-how to set up, and nobody hands it to you by default. Without it, every mistake feels catastrophic, because there’s no easy way to see what changed or undo it if it breaks something. I originally built this for people early in that learning curve, and it’s since broadened well past that—but the core idea held up: the safety net should be built into the tool itself, not something you have to already know how to build.
Part of the reasoning is simple: a costly first mistake is often the reason someone stops learning HTML, JavaScript, or any other language before they really get started. If a tool makes an early mistake expensive or hard to undo, a lot of people just won’t try a second time. I wanted a mistake made while learning to be reversible by default, not something that could quietly cost someone their site, their data, or their motivation to keep going.
I also wanted it to give people real server power, not just a simple static-site option—the same appeal as something like NeoCities, but with more underneath it. That’s part of where the name comes from: simple on the surface, genuinely powerful underneath. That goal is also why the hardware requirements stayed deliberately low—it runs on 512 MB of RAM, on basically any CPU from the last decade, and a Raspberry Pi 4 can run it without strain. If a five-year-old low-power board can run it comfortably, a cheap VPS is overkill, and that was the point: real server capability shouldn’t require real server budget.
I also made it open source and self-hosted on purpose. I didn’t want anyone to feel like they had to keep paying just to learn—or to pay through some in-between period where they weren’t actively building anything, just sitting with it. If the safety net is the point, it shouldn’t come with a meter running.
That’s the real problem. It’s not a spec. It’s a shape of pain, and a decision about who should have to pay for what. Turning that into software meant figuring out what the actual pieces were.
Starting with objects, not screens
The instinct most people have when a project is ambiguous is to start imagining what it should look like—what the dashboard shows, what the buttons do. I don’t start there. I start by asking: what are the actual things that exist in this system, and how do they relate to each other?
For Cute Magick, the objects turned out to be fairly small in number, once I stopped trying to invent new ones and looked at what already existed underneath the problem:
- A site is just a directory on disk.
- A file is an actual file belonging to that site.
- A revisionis a Git commit. Not a custom version-history record I’d have to design and maintain—an actual commit, because Git already does this well.
- The working stateis the site’s Git working tree. The live state is whichever commit is currently marked live. A preview is any other commit, rendered.
- A database and uploads are persistent data that has to survive revisions, so they live outside Git entirely, on purpose.
- A runtimeis whatever executes a site’s files—PHP, Node, Python, Bash, Lua.
- A domain resolves an incoming request to a site. A user controls access to the management interface.
That’s most of the system. Once those objects existed and I knew how they related—a site has files, has Git history, points to one commit as live, can render any commit as a preview, has database and uploads that live outside all of that—a surprising amount of the actual feature set fell out on its own, instead of needing to be separately designed.
Why this matters more than it sounds like it should
The clearest example is something I’d call “Time Machine” in the product—the ability to look back at old versions of a site and restore one. If you start from screens instead of objects, that sounds like a feature you’d have to build: a version-history system, a UI for browsing it, logic for restoring a version safely.
But once “a revision is a Git commit” is true, Time Machine isn’t a separate system at all. It’s just Git history, surfaced. Going live means selecting a commit. Previewing an old version means rendering a different commit than the live one. Restoring an old version doesn’t rewrite history and doesn’t risk destroying anything—it just creates a new commit, the same way any other change would. I didn’t build a version-history feature. I built one honest mapping from “revision” to “commit,” and the feature was already sitting inside Git, which has been solving this exact problem, extremely well, for two decades.
That’s the actual payoff of starting with objects and relationships instead of screens: you’re not inventing application-specific machinery for problems that already have a mature, boring, well-tested solution. You’re finding the smallest accurate concept for each piece, giving it one clear job, and reusing something solid—Git, the filesystem, SQLite—instead of quietly rebuilding a worse version of it yourself.
What I don’t force
I want to be honest that this isn’t a purity test I apply everywhere immediately. There are places in Cute Magick right now—some of the filesystem-handling routes, some runtime-specific logic—that have more orchestration in them than I’d ideally want. I know what the cleaner abstraction would eventually look like. I haven’t built it yet, because the messier version is still working, and pulling a new abstraction out before it’s actually needed usually means guessing at a shape that doesn’t fit once real use reveals what it should have been. I’d rather extract that boundary when the pain of not having it becomes real, not before.
What this means if you’re evaluating whether I can handle something ambiguous
When there’s no existing pattern and no spec, I don’t start by guessing what it should look like. I start by asking what actually exists in the system, and how those things relate to each other. Most of the time, once that’s honestly answered, the architecture—and a lot of the feature set—isn’t something I have to invent. It’s something that was already there, waiting to be named correctly. That’s the same approach I bring into client work at PixelsWithin, whether the ambiguous thing is a product I’m building from scratch or a system I’m inheriting from someone else.