There is a way of thinking about software that I have been circling for years now, refining it with every project, every mistake, every late-night debugging session where I realized the tool I reached for had costs I had not accounted for at the start. The posts on this blog cover a range of topics - databases, infrastructure, tooling, team dynamics, architecture - but they are not separate subjects. They are facets of a single way of working, and this post is where I try to name that way and explain what holds it together.


On the Consensus We Inherited

The software industry has a way of settling on answers without anyone quite deciding that those are the answers. Not formally, not through any process of collective evaluation, but through the slow accumulation of examples and recommendations and what the people around you seem to agree on until the thing that everyone does becomes the thing that everyone assumes is right. Use the cloud, reach for an ORM, adopt a framework with a build pipeline, support every browser that anyone might be using, move fast and break things.

Each of these arrived through a reasonable process applied to a reasonable context. The cloud made sense when managing your own hardware became a distraction from building the product. ORMs reduced repetitive SQL when the team was large enough that consistent patterns mattered more than understanding every query. Build tools let you ship modern code before browsers had caught up to the specification. Browser compatibility was the right answer when your audience was the entire internet. Moving fast was survival when the runway was measured in months and the product had not yet found its place.

The difficulty is not that these answers are wrong. It is that they have drifted outward from the contexts where they were genuinely the best choice and settled into the assumption that they are always the best choice, and a very large category of software has been shaped by that drift without anyone examining whether it fits.

I work on applications that do not live at internet scale. Membership systems for professional associations. Booking tools for community centers. Training records for certification bodies. Scheduling systems for veterinary practices. Internal tools for teams of twenty people. These applications have tens or hundreds of users, not millions. They run on a single machine. They are maintained by one developer, sometimes part-time, sometimes a volunteer.

The tooling and conventions that evolved for products with tens of millions of users and engineering teams of fifty people have been applied to these contexts as if the difference in scale did not require a difference in approach. The Reepolee approach is what happened when I stopped accepting those defaults and started asking what would change if I chose the tools and patterns to fit the actual problem rather than the one the industry was organized around solving.


The Commitments

Let me walk through the ideas that hold this together. They are not a complete philosophy of software development. They are the ones that emerged from real projects, real failures, and real maintenance over years, and they are the ones that distinguish this way of working from what the broader industry currently recommends.

Where the Software Lives

The first decision that changes everything else is where the application runs. And the assumption that it should run on someone else's infrastructure, paid for by the month, scaled by someone else's decisions, is an assumption worth questioning if you have not questioned it in a while.

For the kind of applications I am describing - bounded audiences, predictable workloads, data that belongs to an organization and no one else - a five-euro VPS or a machine in a building handles the load without breaking a sweat. The cost is predictable to the cent. The data stays where the organization decided it should stay, under its own access controls, subject to its own backup policy, governed by the laws of the jurisdiction the organization chose rather than whatever data center region the cheapest cloud tier happened to provision in. When the internet goes down - and it does, regularly, for reasons that have nothing to do with how well the application was built - the application continues to work for everyone on the local network.

There was a time when running your own server required network administration skills that put it out of reach for small organizations. That time has passed. Tunneling tools have changed the equation completely: a lightweight agent running on the server maintains an outbound connection to an edge node, handles SSL termination, HTTP/3, caching, DDoS protection, and exposes the application to the internet without opening a single inbound port. The server is not directly reachable from the internet. The firewall stays simple. The setup takes an afternoon.

Linux's systemd handles the hard parts of keeping a process alive. A service definition that says run this command as this user, restart it if it exits, start it when the machine boots - that is fourteen lines of configuration. Database backups are a script and a cron job. Log rotation, health checks, deploy hooks - all of it is solved at the level of shell scripts and standard Linux tooling that has been stable for decades. Reepolee ships a service definition and installs it with a single command. That is not ops engineering. It is a developer who spent an afternoon reading about systemd.

And the privacy implications run through all of this whether you name them or not. When the data lives on a machine you control, behind a door you lock, on a network you manage, the question of who has access to it has a simple answer. The compliance conversations become straightforward. The audit trail is short. GDPR, CCPA, LGPD - the frameworks keep coming, and the cleanest path through all of them is knowing exactly where the data is and who can touch it.

On-Premise, or at Least Your Own VPS covers the full case: the cost comparison, the control argument, the regulatory landscape, what happens when the fiber gets cut.

Removing the Build Step

A build pipeline is infrastructure. It requires configuration, maintenance, debugging, and upgrades. It sits between you and the running code when something goes wrong, and when something goes wrong is exactly when you least want something between you and the running code. For a small team maintaining an application that serves a few hundred users, the value of that infrastructure rarely exceeds its cost.

Bun changed this more than I think the industry has fully absorbed yet. It runs TypeScript directly. No compilation step. No bundler configuration to debug. No fifteen-minute cold starts in CI while the build tool decides what it needs to do. The database client, the HTTP server, file I/O, WebSockets, password hashing, S3 and blob storage - all of it ships in a single binary. The things that used to require Express and a database driver and a session library and a file upload handler and a WebSocket library are simply there, part of the runtime you already have.

The result is an application that deploys with git pull and restarts with one command. The running server has zero npm packages. Third-party libraries that the application genuinely needs are vendored - downloaded once, inspected, committed to the repository, served directly by the application. No npm install pulls them from a registry at deploy time. No CDN serves them from somewhere else. They are files in the repo, stable, auditable, and present whether or not the registry is up.

This is not minimalism posed as a virtue. It is the elimination of an entire category of failure modes from the deployment path. Every dependency you do not have is a dependency that cannot have a vulnerability published against it, cannot be abandoned by its maintainer, cannot introduce a breaking change in a minor version bump, and cannot be compromised in a supply chain attack that propagates through the tree silently.

No Deps, No Build goes deeper into Bun as an application platform, the economics of dependency management, and why vendoring changes the security posture of a small application more than most developers realize.

What the Database Speaks

There is a tendency in our part of the industry to treat the database as something to be managed through a layer of indirection, as if direct engagement with it is a legacy practice that modern tooling has made unnecessary. I have watched this tendency cause problems for years, and it is worth being direct about why.

The database has its own language, its own performance model, its own semantics for consistency and isolation. Writing a query is not like writing application code - it is set-based rather than record-based, declarative rather than imperative, and the execution plan the database chooses for your query can make the difference between a page that renders in ten milliseconds and one that renders in ten seconds. An ORM does not make these things go away. It makes them invisible, which is a worse outcome, because invisible problems do not get solved.

SQL is not a difficult language. The syntax is small. The concepts - SELECT, JOIN, GROUP BY, transactions - map directly to things you are already doing, expressed differently. What SQL requires is a shift in thinking, and that shift is worth making because the database is one of the most important systems in most applications. A developer who can read an execution plan, write a JOIN instead of N+1 queries, open a transaction explicitly when atomicity matters, and understand what an index covers versus what it does not - that developer controls their data layer rather than being controlled by it. When you cannot read the execution plan yourself, the debugging hours add up fast, and those hours come out of your pocket on a fixed-price project.

Reepolee generates the SQL directly. The query is what runs. When it is slow, you look at the query. When it does the wrong thing, you read the query. When you need to add a condition, you add it to the query. There is nothing between you and the database but the driver, and that lack of indirection is the feature.

The Database Is Not a JavaScript Object makes the full case for SQL fluency. And Your Database Is Probably Permanent extends the argument further: once you have committed to a database and it has accumulated years of production history, the idea that you will switch it out for another one is mostly a comforting fiction. Use what it offers - procedures, views, triggers, native functions - rather than withholding those capabilities to preserve a portability option you will almost certainly never exercise.

Generating What Can Be Generated

Large language models are extraordinary tools. I use them daily. They are exceptional at reasoning through novel problems, synthesizing context, understanding what someone is asking for and routing it to the right place. They are the wrong tool for generating routine business application code that has a correct and stable answer.

The reason is structural. An LLM produces different output from the same input depending on context, phrasing, time of day, and factors that are hard to characterize fully. The result across a codebase built primarily through AI generation is inconsistency: subtle variations in patterns that should be identical, bugs that appear in slightly different forms across dozens of files, a gradual drift away from the conventions the team agreed on because the model was never given those conventions in a way it could apply deterministically.

A generator does not have this problem. It reads the schema - the actual database schema with your column names, your types, your foreign keys, your constraints - and from that single source of truth it produces, consistently and repeatably, the artifacts that schema implies. Routes, SQL queries, form templates, validation schemas, type definitions, translation keys. Same input, same output, every time. A bug in the generated pattern is fixed in one place and every resource inherits the correction.

The right use of an LLM in this context is to help build the generator - to encode your domain knowledge into a tool that then produces correct code forever, without paying per-token for every output. Use the model for what it is good at: understanding a description of what you need, reasoning about the context, and routing the request to the generator. Let the generator do what it is good at: producing deterministic, auditable output from a known-good pattern.

Stop Calling the LLM for Things That Don't Need Thinking develops this argument in detail, including the role of MCPs as the bridge between the LLM and the generator. Quality Doesn't Matter Until It Does explains why consistency is what business software actually buys and why the failure modes are different when correctness matters for transactions rather than engagement.

Organizing by What the Software Does

There is a reflex in web development to organize code by what it is technically. Models in one folder, views in another, controllers in a third, routes somewhere else entirely. It made sense as a teaching tool for explaining the architecture of a web application. As a folder structure to work in day after day, it creates a cognitive tax that compounds with every file added and every developer who joins the project.

When you sit down to work on a feature - users, invoices, orders - the unit of your attention is the feature, not the layer. All the code you need to understand and change that feature should be reachable without leaving its context. When the users feature lives in one folder - the route, the SQL, the template, the validation, the translations - opening that folder is opening the feature. You do not need a map. The file system is a map of the domain, and the domain is what you are thinking about when you are working.

Deleting a feature becomes deleting a folder, and the feature is gone completely by construction. A pull request for a new feature touches one part of the tree, readable in sequence, reviewable without a mental map of the project. A developer who has been on the project for two days can understand a feature by looking at one folder.

The same instinct applies inside the template. The .ree format keeps what the browser needs - markup, styles, behavior - in one file, readable top to bottom, self-contained. Server-side code lives right next to it in the same folder, close enough to understand in a single directory listing, but separate because the concerns genuinely are different.

MVC Separated the Wrong Things explores feature-per-folder architecture, single-file templates, Tailwind's elimination of CSS indirection, and the principle of code proximity in more depth.

Simplicity Compounds

I have spent enough years maintaining software to develop a deep respect for the things I did not add. Every layer of complexity in a system is a layer that can fail, a layer someone has to understand, a layer between you and the running code when something goes wrong at the worst possible moment. This is not an argument against all complexity - some of it is necessary and genuinely valuable. It is an argument for making every layer earn its place.

Default settings are not laziness. They are the configuration the tool's authors designed, tested, and optimized through thousands of hours of use. The developers who have customized everything sometimes struggle most when they sit down at a machine that does not have their configuration. The portability of your skill with the defaults is worth more than the comfort of your customized setup, and worth thinking about when you reach for the configuration file.

Snake_case from the database through to TypeScript is not a stylistic preference. It is a visual marker that tells you at a glance which values came from your domain and which came from libraries or external APIs. It eliminates the transformation layer between what the database returns and what your code uses. It makes your naming conventions searchable with grep. The underscore tells you this is yours.

Failing loudly when an environment variable is missing is not pessimism. It is the fastest possible signal that something is wrong, delivered before any data has been touched, any email has been sent, any invoice has been generated. The application should refuse to start. It should say what it needs and stop. The developer who sees that message fixes it in thirty seconds and moves on.

Light and dark mode - both, with system preference as the default and a per-site toggle stored in a cookie so the choice survives visits and the theme is set before the first byte renders - is not an extravagance. It is thirty lines of code and two CSS custom property values, and it respects what the user told their operating system while still letting them make a different choice for this specific site.

Each of these is small. Together they accumulate into a codebase that is simpler than it would have been, more maintainable than it would have been, and less likely to surprise the person who has to debug it at 2am.

Default Is a Feature explores the cost of customization and the portability of defaults. Fail Loudly Env Vars makes the case for crash-early validation. Name Your Columns Like You Mean It establishes the naming conventions that make a schema readable. Light, Dark, Do Both shows the two-layer pattern for theme support.


What This Is and What It Is Not

Let me be clear about the boundaries, because every approach has them and pretending otherwise helps no one.

This is not a rejection of cloud infrastructure. Cloud services are clearly the better fit for products that serve the global internet, need to scale unpredictably across regions, or benefit from managed services that would require a dedicated operations team to replicate. The argument is about defaults and context - about asking the question honestly rather than accepting the assumption.

The same goes for the tools that make modern development faster. I use LLMs daily - to help construct generators, to reason through novel problems, to synthesize the context of a codebase I have not touched in six months. The question is always where their probabilistic nature adds value and where it subtracts it.

And this is not nostalgia for an earlier era of computing. Bun, modern CSS, native browser APIs, deterministic code generation, tunneling for on-premise deployment - this approach leans into genuinely new capabilities that did not exist five years ago. The difference is in which capabilities are chosen and why. A full-featured framework wrapping an internal tool with three routes is overhead that never earns its keep over the life of the project, but that is not the framework's fault. It is a mismatch between the tool and the problem. The problem is what should drive the choice.


The Audience

Let me name directly who this is written for, because it matters for how you read everything that follows.

You are a freelancer. A solo developer. A small agency of two or three people. An informal group of developers who work together on projects because you share the same convictions about how software should be built. You are the one who gets the call when something breaks, who carries the maintenance cost in your calendar, who has to explain to a client why the monthly hosting bill went up again. You care about simplicity not because it is aesthetically pleasing but because complexity is time you do not have and money you are not being paid for.

The organizations you build for - trade unions, housing cooperatives, sports clubs, alumni associations, municipal offices, small clinics, volunteer networks - need software that works, that costs what they expect it to cost, that keeps their data where they decided it should stay. They do not have an engineering team. They have you. And you have a finite amount of attention to spread across their projects.

For the organizations you serve, the honest comparison is not this approach versus a modern cloud deployment with CI/CD and container orchestration. It is this approach versus the spreadsheet it is replacing, or the off-the-shelf system that costs ten times as much and does half of what is needed, or the volunteer-built solution that has been held together with tape for three years and is finally breaking. Against that comparison, an application that runs on a five-euro VPS, deploys with a git pull, keeps data on hardware the organization controls, and asks you to think about it for an afternoon once a quarter is not a compromise. It is the right tool - for them, and for you.


The Bet

Reepolee is organized around the conviction that code generation from schema, Bun as the platform, SQL as a skill rather than something to be abstracted away, feature-per-folder organization, on-premise or VPS deployment, and zero runtime dependencies form a strong foundation for a specific and very common category of software that the industry consensus has been underserving for years.

Some of these commitments will look obvious in five years. One or two might not hold up. That has always been the shape of this kind of work. The alternative - waiting for the consensus to validate each choice before building, reaching for what everyone else has already approved, staying inside the boundary of the well-documented and the safe - produces software that is fine and forgettable in equal measure.

The rest of this blog works through the details. Each post examines one piece of the puzzle - the database, the deployment, the tooling, the team, the architecture - and lays out why this way of thinking about development and long-term operations produces software that works correctly and keeps working.

The developer who spends a decade maintaining something understands complexity differently than the developer who spent a sprint building it.