← Back to list

I Built an App in One Afternoon, Then Spent Two Weeks Finishing It. Here Is What I Learned.

PillarPlan was almost done after a single Lovable session. The next two weeks were the data model rework, the invitation flow, and the…

Eddie Larsen · 2026-06-11 16:01 · 0 claps · 7.4 min read
#vibe-coding #solopreneur #saas #ai-tools #entrepreneurship
Open on Medium ↗
Wiki topics: AI · AI · General STP · Startups & Venture 💻 · Programming

I Built an App in One Afternoon, Then Spent Two Weeks Finishing It. Here Is What I Learned.

PillarPlan was almost done after a single Lovable session. The next two weeks were the data model rework, the invitation flow, and the security work, which the first afternoon never asked about.

The first session built the facade. The structural work the facade hid was the next two weeks.

The first session built the facade. The structural work the facade hid was the next two weeks.

There was an afternoon in April when I thought I had built PillarPlan. The thought stayed intact for about a week, until the data model fell apart and made clear how much I had not built.

The session went better than I had any right to expect. From the moment I approved the build plan in Lovable to the moment the dashboard rendered with real data, it took about three minutes. You could create an organization, define up to five strategic pillars, log SWOT entries tagged to each pillar, capture PESTLE external factors, and watch it all roll up into a clean executive dashboard. The interface looked clean. The flows worked. The data persisted across reloads. The screenshots looked like a real product.

I told myself the app was basically done. The next session, I figured, would just be layering on objectives, initiatives, scoring, and the execution loop. That was the optimistic version of the project. The actual version took the next two weeks of focused work to even reach a state I trusted to put in front of a paying customer. None of those two weeks were spent building new features. They were spent fixing things I had not noticed were broken.

This is what hitting the eighty percent wall actually feels like when you are vibe coding a real SaaS product. The first session is the magic moment. The rest is the work the magic moment hid.

What the first session actually produced

PillarPlan is strategic planning software for mission-driven organizations. Nonprofits, FQHCs, school districts, credit unions. The premise is that most of these organizations write a strategic plan once a year, in a Word document, and never look at it again. PillarPlan turns the plan into a living system the team can actually use throughout the year.

The first session established what I have since come to call the Strategic Planning Core. The user creates an organization. They define up to five pillars, which are the strategic priorities the plan is organized around. They log SWOT entries, the strengths, weaknesses, opportunities, and threats that show up in any real planning conversation, and tag each one to a pillar. They capture PESTLE factors, the political, economic, social, technological, legal, and environmental forces shaping the org. All of it rolls up into an executive dashboard that shows the plan at a glance.

Lovable built that whole loop in one session. I had done the planning work first, which I have learned to always do, and the prompts came back as working code. The Supabase tables looked right. The dashboard rendered. I could create an organization, walk through the full setup, and see the dashboard fill out with real data. The product had a heartbeat.

That heartbeat was the part that fooled me. A working dashboard turned out to be a thin layer over everything I had not yet built.

The day the data model fell apart

The first signal that I was not done came when I tried to invite a second person to an organization. The invitation worked. The user record was created. The second person could log in. None of that meant the system actually supported them as a second user.

The original data model put roles and subscriptions on the profiles row, which is fine for a single-user account. As soon as a second person needed to join an org, it stopped working. A user could belong to only one organization, with one role and one subscription. The model could not represent the actual shape of the product.

Around April 13, I refactored the model to be organization-centric. Organizations got their own owner_id. A new org_members table was added with user_id, org_id, and role columns. Subscription ownership moved from the profile to the organization. That is the kind of change that looks small on paper. In practice, every downstream row-level security policy had to be reworked to scope by org_id via a helper function called get_user_org_id, rather than trusting the role field on the profile. Every query that depended on the old model had to be rewritten. The migration ran in a few minutes. The downstream rework took most of a day, then another day to verify nothing had silently broken.

That was the first weight of the eighty percent wall. The product worked, but only for the case I had built first. The other cases were going to require structural work to support.

Invitation flow, death by edge cases

The next ten days were the invitation flow. There was no single bug to fix. There were three interlocking ones, each one looking simple on the surface.

PillarPlan needs to let an org admin invite team members. That sounds simple, and it is, until you actually run it.

The first problem was that I had unknowingly built two parallel invitation systems. A legacy invitations table from an earlier version, and a new org_invitations table from the data model rework. A trigger called enforce_profile_role_on_insert was checking the legacy table to set a user’s role on signup. The new invitations did not trigger that. Anyone invited as a viewer through the new system silently got promoted to admin when they signed up. I caught this when a test invite produced a user with permissions they should not have had.

The second problem was the signup handler, called handle_new_user, which had to deal with three different signup paths in a single function. A user signing up through a legacy invite. A user signing up through a new org invite. A user creating a brand-new organization that had to auto-create the org, the profile, the org_member record, and a 14-day trial subscription, all in one transaction. The original version of this function only handled the first case. Adding the other two without breaking the first one took longer than I want to admit.

The third problem was the accept-invite flow itself. The /accept-invite page needed a state machine because an invited user could land there in one of five states. They could already be signed in as the right person. They could be signed in as someone else. They could be brand new. The page could be loading. The flow could error out. Each of those states needed its own branch, and the error states needed to be recoverable without forcing the user to restart the invite.

The pattern across all three problems was the same. Each one looked simple on the surface. Each one was an iceberg underneath.

The security scanner finding

Late in the second week, a security scanner caught something I had missed. The scanner was running against the live preview environment, which is what scanners are for. It flagged a real vulnerability, which I had not been thinking about.

Finding number 577 was that any authenticated user could insert into the profiles table with an arbitrary role. That meant a user could sign up as a basic user, then post directly to the database and promote themselves to admin. The vulnerability was sitting there the whole time, on a SaaS that was supposed to handle strategic plans for organizations that care about who can see them.

The fix was three security-definer triggers. The first, enforce_profile_role_on_insert, derives the role from the invitation table and never trusts whatever the client sent. The second, prevent_profile_escalation, makes the role and organization_id immutable on self-update. The third, prevent_admin_role_escalation, blocks an org admin from promoting another member to admin or moving them between orgs without the right authority. Three triggers, one afternoon to write, one more afternoon to test.

None of those triggers existed in the original build. The AI did not write them because the AI did not know how to write them. I did not ask for them because, in the first afternoon, I was thinking about pillars, SWOT, and dashboards, not about what an authenticated user could post directly to the database.

The security work was not glamorous. It produced no visible feature. It was also the work that made the rest of the product real.

The visible product was a small share of the actual product. The rest was underneath, waiting to be built.

The visible product was a small share of the actual product. The rest was underneath, waiting to be built.

What the eighty percent wall actually was

PillarPlan worked after the first session. The Strategic Planning Core was real. A user could log in, define pillars, walk through SWOT and PESTLE, and see the dashboard come together. The screenshots looked good. The flows worked for the user who had built them.

What the first session had not built was the case for every other. The second user. The invited admin. The viewer who tried to access pages they were not authorized to access. The authenticated stranger is trying to write to the profiles table. The wrong-signed-in user landing on an accept-invite page. The user who tried to access a different organization’s plan. Each one of those cases was a real edge of the product. None of them had been part of the prompt that built the first version.

The two weeks of work after that first afternoon were the actual shape of the work, finally visible. The first afternoon built what the AI knew how to build. The two weeks covered what the AI did not know to ask about, because nobody had told it that PillarPlan would have multiple users, with multiple roles, signing up through multiple paths, with a real security model underneath them.

Vibe coding finishes the easy eighty percent quickly. The remaining twenty percent is the work that the easy eighty percent hides from view. The first afternoon felt like ninety percent. It was closer to fifty.

Closing

PillarPlan is in production now. The data model is organization-centric. The invitation flow has a state machine. The security triggers are in place. The product handles the cases that the first afternoon could not see.

If I were building it again, I would still vibe-code the first afternoon because that is where the leverage is. I would also know, from minute one, that the first afternoon is not the whole project. I would budget two weeks for what comes after, not the few days I assumed last time.

I wrote a longer guide on this for State of Make at stateofmake.co, with the five disciplines that have made the most difference in my own work. If the eighty percent wall is a wall you have hit before, the guide is where the full framework lives.

Thanks for reading, now go build something fun.

Eddie

P.S. If you want a smaller starting point for building your own Claude Code tooling, I put together three skills I use every week. They are not the same discipline, but they are part of the same idea: building your own AI tools instead of just using what comes out of the box. The bundle is at https://e2larsen.gumroad.com/l/the-solopreneur-skills-stack.


메타데이터
post_id
545a36a52a3b
slug
i-built-an-app-in-one-afternoon-then-spent-two-weeks-finishing-it-here-is-what-i-learned-545a36a52a3b
url
https://medium.com/@e2larsen/i-built-an-app-in-one-afternoon-then-spent-two-weeks-finishing-it-here-is-what-i-learned-545a36a52a3b
canonical_url
https://medium.com/@e2larsen/i-built-an-app-in-one-afternoon-then-spent-two-weeks-finishing-it-here-is-what-i-learned-545a36a52a3b
author_url
https://medium.com/@e2larsen
status
ok
fetched_at
2026-06-20 20:29:01