← Compendium

Free doesn’t mean careless: a web app after a year in production

Everyone knows how to build a web app “properly”: accounts, email confirmation, tracking, everything polished for launch. Early in 2026 we deliberately broke with almost all of it for a new product idea: free for good, no sign-up, no data hoarding. What can you take from a year of our new normal for your own product?

A good year ago we launched grounds-up.coffee, a tool for meetings and retros that actually reach a result. It deliberately breaks with almost everything that counts as a must when you build a web app "properly". It costs nothing and will stay that way, it asks for no sign-up, and whatever people type into it lives only as long as the purpose requires. By the usual maths, a product that earns nothing directly and binds no users should save on everything else.

That was not the plan. It is not our first application; we know what a product needs that people use every day. Two things were new: this application was built entirely with AI from the first line, and it was planned as free for good from the start. Neither was meant to change the standards we apply to every production product for a client.

The diligent option is the cheap one, if you leave out the right things early.

That sounds like a contradiction, but it is the one insight this year confirmed most clearly. Almost nothing that kept operations secure and cheap was an extra tool or a bigger team. It was almost always a decision to leave something out, and to do so early, before it gets expensive. The stations below show what that means in concrete terms, from the sign-up through the data to a threat landscape that spares no one in 2026.

The login that did not need to be one

Picture someone who wants to start a retro with four people in two minutes and lands first on a registration form, then in their inbox, hunting for the confirmation mail. Half the group is distracted before the meeting even begins. The obvious question: how can access be secure if we drop exactly this sign-up?

The answer starts by separating two things that often get lumped together. An account answers who you are permanently. For a meeting all that counts is whether you belong there right now, and that can be answered without a lasting identity. Whoever moderates creates a room and holds a key for it that lives only in their own browser. Every participant automatically gets their own on joining, with no sign-up at all. Entry is requested via the room code, and the moderator decides who comes in and who flies out again. The code is shared on the channel the group already uses, over Slack, over mail, or simply in the video call everyone is already sitting in. No account, no password, no email ping-pong before you may finally get going.

So much for the idea, which carries over to many products. The real diligence sits a layer below, and it shows itself in a step everyone knows from such meetings: voting.

Casting a vote, explained by example

In a retro the group collects topics and then votes on which come first. Everyone has a limited number of votes. In the browser the interface makes sure nobody spends more points than they have; the surplus simply cannot be clicked any more. That is normal and right, but it is only half of it.

Because the interface runs on the participant's device, and there you can change anything. Whoever wants to bypasses the browser limit with a few moves. So on saving, the server checks the same thing again: does the vote belong to a real participant of this room, and do they even have any left? Many grasp this too, the server is the binding instance, not the browser.

The part that is easily forgotten comes now. The permission to save a vote is not permanent, it is tied to a time window. It holds only while the voting phase is open. The moment the moderator closes it, nobody may save any more, not even the person who was perfectly entitled to vote a second ago. "May vote" feels like a firm possession, but it is really a right on loan for a while. Whoever overlooks that builds a check asking "is this a participant?" and forgets the second one asking "is voting even open right now?".

Participant casts a vote save? Server checks 1 · real participant of this room? 2 · any votes left? 3 · is voting still open? the right holds only while the phase is open accepted or rejected Moderator only sees: 14 votes
The same vote, checked three times, and the third check is the one often forgotten: the right to save holds only while voting is open. The moderator in turn sees only how many votes there are, not from whom, because more is not needed for that purpose.

And one more thing hides in this small step. The moderator wants to see how the vote is coming along, so they get the number of votes cast so far. But only the number, not who voted how, because for the purpose of moderating the round that is entirely irrelevant. The permission is thus limited not only in time but also in scope: everyone sees exactly as much as their role needs, and not a bit more.

That is the pattern behind almost every secure feature, and it is bigger than voting. The question is rarely just "who may?", but "who may, for how long, and how much does the other side even need to see?". Think that way and you get most decisions right on your own, long before it comes to concrete technology.

That access is not a permanent possession but hangs on purpose and time is an idea that carries far beyond a meeting tool. Why it matters more with every AI tool in the building, we explore in a separate article.

Data that cannot be traced back

Every product faces the same temptation at some point: you want to understand how it is used, and the easiest way seems to be to record as much as possible. We wanted the opposite, to keep as little as possible. That sounds contradictory, because how do you learn from usage without looking over the users' shoulders?

A meeting tool inevitably sees what people write into it, and as long as the room lives, those contents sit stored so a restart does not destroy the session. The difference lies in what happens afterwards. Once the room expires, it is deleted automatically along with its contents, without anyone pressing a key.

What remains is a statistic that deliberately cannot be traced back to a person. When a room disappears, it is recorded how long the phases took and how many topics were discussed, but not what was discussed or by whom. Accesses to the application are counted too, but the detail that would reveal a single visitor, their address on the network, is not stored in the first place. Only that an access happened is counted, not where from. What never lands in the statistic can also not be reassigned to a person by any later match.

The surprising part is that this very restraint produced the clearer view. Because every room created leaves an anonymous dot, you see the whole funnel: how many rooms opened, how many actually started, and how many were carried through to the end. Where many drop off, something is wrong, and that shows without having looked over a single person's shoulder.

rooms opened meeting started to the end each step only an anonymous number, no content, no person
The usage funnel emerges purely from anonymous counts. Where many rooms open but few are carried through, there is a hurdle you can address without evaluating any user data.

This data minimisation is no afterthought coat of data protection for us, but a principle we work by anyway. We run and build our products along the principles of ISO 27001, even without a certificate on the wall. Collecting only what a function really needs and getting rid of it once the purpose is served is exactly such a principle. That no revenue hung on the data made the decision easy: there was no reason to keep anything we might later have wanted to make money from. The principle protects the users, and in passing it protects us, because data that is not stored in the first place cannot go missing.

Why even the small app is a target today

In the first days of operation, a look at the accesses showed something: a large share came not from users but from automatic scanners searching at random for known holes. That raises an uncomfortable question, especially for a small, free tool: why does anyone even knock, on a product that should be a worthwhile target for no one?

The attack landscape has fundamentally turned. An attack used to need a target worth the effort, because behind the keyboard sat a human whose time cost money. A small, free tool was simply too unimportant to come into view.

That has reversed. Automation, and by now AI, have pushed the cost of an attack per target down so far that it pays to simply knock on the entire internet and take whatever answers. A documented case from autumn 2026 shows the order of magnitude: a single actor broke into 27 companies in five days using freely available AI agents, at a cost of on average around 25 US dollars per target (Dark Reading, 2026). At that price per target, anyone who is online is a target, no matter how small or inconspicuous.

How real this is showed itself at the end of 2026 in an unexpected place. In October and November, across the borders of many European countries, the internet-connected refrigeration controllers of hundreds of supermarkets were attacked, temperatures turned up, goods spoiled (Palo Alto Unit 42, 2026). No one was after a particular branch. Being vulnerable and reachable was enough, and researchers found thousands of such controllers open on the net (Euronews, 2026).

Against this kind of pressure no big security team helps, but a sober way of building does. The application sits behind several layers, each with one job. An upstream CDN with a web application firewall catches the crude nonsense far outside. Behind it a load balancer takes the connections and rebuilds them before anything reaches the container. And the container itself runs on a deliberately minimal, up-to-date base image that offers barely any attack surface.

What it means that the load balancer rebuilds the connection can be said without jargon. A request from the internet is like a parcel that passes through many hands, and in every layer of the wrapping a trick can hide that throws a poorly built system off balance. Because the CDN and load balancer unpack the parcel completely and tie it up cleanly anew, the container never sees the raw parcel from the street, only a checked, well-formed content from inside its own house. Nothing can ever be ruled out entirely, but the probability of exploiting something on this level becomes very low. What remains is the application itself, and there the clean dependency tree and the firewall in front of it take hold.

CDN + WAF catches Load balancer repacks Container minimal image
Each layer shrinks what the next can even see. Taking away attack surface is more effective than guarding it after the fact.

And because the mass comes automated, what counts above all is speed. The answer to the scanners from the first days was therefore not a human intervening at night, but a rule that locks out a conspicuous address on its own for a while, together with regular, automatic checks that run anyway. Keep the attack surface small and react fast and automatically: both are again leaving out and prevention, not arming up.

Someone else's code is borrowed risk

The attack surface is not only what is reachable from outside, but also what you bring into the house as someone else's code. Monday morning, the security scanner reports a weakness in a package buried deep in the dependencies. The reflex is to quickly pull in the newest version and move on, tick the box. We pause here and ask a different question first, and this habit has turned out to be one of the load-bearing ones.

Before updating, we take inventory. What purpose does this library serve, is it even still maintained, and do we use its full scope or only a small part? Often the honest answer is that it does a trifle you could just as well write yourself, and then it goes. If it cannot be removed, perhaps it belongs only in development and not in the shipped product, which takes it out of the line of fire. Your own code is not automatically safer than a library, but in your own context it is easier to review and to own.

An example from our own operations: to monitor the licenses in our projects, the tool license-checker ran for a long time. It has not been maintained for years, which you can tell already from the community splitting it into numerous forks. With that lack of maintenance it dragged along, through its own dependencies, outdated packages some of which were reported as vulnerable, of all things a tool meant to ensure diligence. The consequence was a switch across all projects, and that is exactly the kind of decision an inventory triggers and a quick fix overlooks.

That maintenance of all things tips the scale is no side note. Open-source libraries are great; they hold a lot of knowledge and work no one sensibly wants to do again. The same library turns into a risk, though, the moment no one maintains it any more, because then known holes stay open and no one reacts. On top comes a firm eye on the licenses: every library in use has to be compatible with the product, and for that we have a standard we apply consistently.

That the application came together entirely with AI from the first line changes none of this. AI lowers the hurdle to building, not the one to operating: separate environments, a checking pipeline and clean dependency hygiene stay decisions you make and own yourself. How starting small with AI works, we show in Starting small in the age of AI, and how delivery changes when AI helps along, in Continuous Integration in the age of AI.

What must stand from day one, and what may mature

For all the diligence, one reassurance is in order. Not everything has to be perfect at launch, and the art lies in telling one from the other. Security and data protection have to sit from day one, because a mistake there is expensive and hard to heal. The feel of an interface, by contrast, may grow over time, and that is exactly what happened.

The shortest path to it is a direct feedback channel in the product itself. From the start there is an uncomplicated way to get rid of a bug or a wish without leaving the tool and without hunting for a form on some other site. In our view that is one of the most important features of all, and it should be among the first you build, not the last. In practice a message often came in during the morning and was back out as an improvement a few days later, one that then helped everyone and not only the one person who had spoken up.

Security brooks no delay; the interface may mature.

A small example shows how the two play together. That every input is checked server-side was set from the start, and not only on the content but also on the length of a field. What was missing at first was the feedback to the person in front of it: the server refused an overly long input only after someone had typed their fingers raw. The maturity then lay not in a new rule, but in a plain "321 of 500 characters" right in the field, derived from exactly the limit the security check already knows. The real work was not this one counter, but carrying it through consistently for every field, so the interface promises everywhere the same thing the server enforces in the end.

What you can take away

The most surprising insight from this year is that almost nothing that carried costs more than a decision at the right moment. Not building a login is cheaper than building one. Not collecting data is cheaper than protecting it. Laying the line to the user early is cheaper than guessing later why no one stays. Whoever waits until a product is "important enough" for diligence builds the expensive variant in the end.

That we could see this so clearly came from a decision we made at the very start and then held to. We wanted the product because it helps us and solves a real problem, but not at any price. So the operating budget was defined quickly and tightly, while the development effort was deliberately not capped, though with a clear brief: assess and act on customer feedback fast. That a budget is less a number than a decision about how much risk you take where, we explore in a separate article.

And one more thing the year showed: 2026 was no quiet year to be online. The tools you build with today are the same ones others attack with, and both have become faster and cheaper. Exactly for that reason a way of building that offers little surface and reacts fast carries further than one that builds big and secures only afterwards. Less is not the frugal choice here, but the robust one.

much surface, much to guard leave out little surface, little to attack
The same motif as in operations and in security: what you leave out, you do not have to guard. Less surface is the more robust way of building, not just the more frugal one.

We do this every day, for our own products like Grounds Up and for clients whose product carries their business, from a clean way of building to readiness towards an ISO 27001 certification.

If you want to know where your own web app stands on exactly these points, we look at it together in a health check: access and data holding, dependencies and attack surface, operations and monitoring. At the end you have a clear assessment and a manageable list of next steps, ordered by impact.

Frequently asked questions

Doesn't dropping sign-up invite abuse?

Without an account there is no lasting name, but there is still control. In Grounds Up the moderator decides who may enter a room and who flies out again, and automatic limits throttle anyone who creates too many rooms in a short time. An account would hardly prevent abuse anyway, since throwaway addresses are made in seconds; it only shifts the hurdle onto the honest user.

If the check in the browser counts for nothing, can I skip it?

No, but its role is a different one. The browser check is there for comfort, such as the character count in the input field or the votes greyed out once none are left, so a person knows early where they stand. You must never rely on it, because anyone can bypass it. The binding check belongs on the server and should ideally be derived from the same rule, so both say the same thing.

What does "access is time-limited" mean in practice?

That a right is tied to a state, not to identity alone. When voting, a participant may only save while the voting phase is open; afterwards the same attempt is rejected. The server thus checks not only who is asking, but also whether the action is even intended right now. This is easily forgotten and is exactly the gap through which later manipulation otherwise slips.

Is this effort worth it even if my product makes money?

Especially then. The article shows on a free product that the diligent option is usually the cheaper one if you choose it early: not building a login is cheaper than maintaining one, not collecting data cheaper than protecting it. With a paid product, revenue and reputation are additionally at stake, which makes the early investment even more worthwhile.

Dropping a library over a CVE sounds like more work than an update.

Short term yes, long term rarely. An update has to be redone at every further CVE; a removed dependency never again. Often a library only does a trifle anyway, which is easier to review as your own, manageable code. If it cannot be removed, a useful interim step is to move it out of the shipped product into development only.

Why would anyone even attack my obscure little app?

Because it costs them almost nothing. Attacks today run automated and AI-assisted across the entire internet, at a few dollars per target. No one is after you personally; being vulnerable and reachable is enough to get swept up. So what protects you is not obscurity, but a small attack surface and fast, automatic response.

My web app is already live. Where do I start with these points?

By impact, not by effort. The most comes usually from the data question, because data you do not collect never has to be protected or lost, closely followed by the attack surface, meaning removing unused dependencies and open access paths. Logins and the interface come after. No one has to tackle everything at once; a health check orders the steps for the specific situation.

Related topics