Back to Home
Why I'm Killing My SaaS Before Launch

Why I'm Killing My SaaS Before Launch

Jan 17, 2026 · By Ege Uysal

Two weeks. That's how long it took me to build a billing system.

Not the entire product-just billing. Subscription tiers, usage tracking, webhook handlers, invoice generation. Two weeks of fighting Stripe's API, debugging webhook signatures, and building retry logic for failed payments.

Meanwhile, the rest of my SaaS-the actual product-sat at 70,000 lines of code across 350 files. A developer productivity platform that connected Slack, GitHub, email, and project management tools into one unified workspace. The thing I'd spent two months building from scratch.

And that's when I knew I had to kill it.

What I Actually Built

Let me be clear: this wasn't a side project. This was production-grade enterprise software.

The scale:

  • 70,000+ lines of code
  • 350+ production files
  • Full CI/CD pipeline with multi-stage deployments
  • 3 containerized instances with load balancing
  • Red-green and rolling deployment strategies
  • Custom email queuing system
  • Modular architecture with dependency injection
  • 100% test coverage with interface mocking
  • Comprehensive documentation (dev-only frontend docs)
  • Cyclomatic complexity linting
  • Multi-tenant organization system with RLS

The tech stack:

  • Frontend: Next.js 16.1.3, React, TypeScript, Tailwind CSS, shadcn/ui, Radix UI, Framer Motion
  • Backend: Go, PostgreSQL, SQLc, PGX
  • Infrastructure: Docker, GitHub Actions (GHCR), Caddy, custom VPS deployment
  • Services: Stripe, Supabase, Sentry, hCaptcha
  • Tooling: Turbopack, Makefiles, automated testing pipelines

This wasn't a startup. This was infrastructure that could handle millions of users. The kind of system worth $500k+ if you hired a team to build it.

And that was the problem.

The Over-Engineering

Here's what over-engineering actually looks like. This is from my organization invitation system-just one small part of the entire codebase:

// InviteMember creates an invitation for a user to join an organization. // //nolint:gocyclo // Complex validation logic with multiple checks func (s *Service) InviteMember(ctx context.Context, userID, orgID uuid.UUID, req InviteMemberRequest) (*InviteMemberResponse, error) { // Validate email format if req.Email == "" { return nil, apperrors.ValidationFailed("Email is required"). WithDetails("email", "email address is required") } // Basic email validation emailPattern := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) if !emailPattern.MatchString(req.Email) { return nil, apperrors.ValidationFailed("Invalid email format"). WithDetails("email", "please provide a valid email address") } // Validate role if req.Role != roleOwner && req.Role != roleAdmin && req.Role != "member" { return nil, apperrors.ValidationFailed("Invalid role"). WithDetails("role", "must be one of: owner, admin, member") } // Check if user has permission (must be owner or admin) member, err := s.repo.GetOrganizationMember(ctx, organizations.GetOrganizationMemberParams{ OrganizationID: pgtype.UUID{Bytes: orgID, Valid: true}, UserID: pgtype.UUID{Bytes: userID, Valid: true}, }) if err != nil { return nil, apperrors.Forbidden("You do not have access to this organization") } role := string(member.Role) if role != roleOwner && role != roleAdmin { return nil, apperrors.Forbidden("Only owners and admins can invite members") } // Check if invitation already exists for this email existingInvitation, err := s.repo.GetPendingInvitationByEmail(ctx, orgID, req.Email) if err == nil && existingInvitation != nil && existingInvitation.Status == organizations.InvitationStatusPending { return nil, apperrors.ValidationFailed("Invitation already sent"). WithDetails("email", "an invitation has already been sent to this email address") } // Convert role to enum var roleEnum organizations.OrganizationRole switch req.Role { case roleOwner: roleEnum = organizations.OrganizationRoleOwner case roleAdmin: roleEnum = organizations.OrganizationRoleAdmin case "member": roleEnum = organizations.OrganizationRoleMember default: return nil, apperrors.ValidationFailed("Invalid role"). WithDetails("role", "must be one of: owner, admin, member") } // Create invitation (expires in 7 days) expiresAt := time.Now().Add(7 * 24 * time.Hour) invitation, err := s.repo.CreateInvitation(ctx, organizations.CreateInvitationParams{ OrganizationID: pgtype.UUID{Bytes: orgID, Valid: true}, Email: req.Email, Role: roleEnum, InvitedBy: pgtype.UUID{Bytes: userID, Valid: true}, ExpiresAt: pgtype.Timestamptz{Time: expiresAt, Valid: true}, }) if err != nil { logger.Error(ctx, "Failed to create invitation", err, map[string]any{ "organization_id": orgID.String(), "email": req.Email, "role": req.Role, }) return nil, apperrors.Internal("Failed to create invitation") } logger.Info(ctx, "Invitation created successfully", map[string]any{ "invitation_id": uuid.UUID(invitation.ID.Bytes).String(), "organization_id": orgID.String(), "email": req.Email, "role": req.Role, "invited_by": userID.String(), }) // Send invitation email asynchronously invitationID := uuid.UUID(invitation.ID.Bytes) s.sendInvitationEmailAsync(ctx, userID, orgID, invitationID, invitation.Email, string(invitation.Role), invitation.ExpiresAt.Time) return &InviteMemberResponse{ InvitationID: uuid.UUID(invitation.ID.Bytes).String(), Email: invitation.Email, Role: string(invitation.Role), ExpiresAt: invitation.ExpiresAt.Time.Format(time.RFC3339), }, nil }

That's just for inviting someone to an organization. Over 60 lines of validation, permission checks, error handling, structured logging, and custom error types.

Here's what it should've been for an MVP:

// Simple Supabase approach const { error } = await supabase.from("organization_invitations").insert({ organization_id: orgId, email: email, role: role, invited_by: userId, }); if (error) throw error; await sendInvitationEmail(email, orgName);

Eight lines. Versus 60+ lines of Go with custom error handling, detailed validation, structured logging, and async email sending.

To be clear: if I were building this today at scale, I'd still choose Go. Custom backends scale better, give you more control, and handle complex business logic elegantly. But I wouldn't write 70,000 lines. I'd write maybe 10,000-focused on the core business logic that actually differentiates the product, not reimplementing what Supabase already does well.

What I Built vs. What I Should've Built

What I BuiltWhat I Should've Built
Custom Go backend with service layersNext.js API routes (migrate to Go later)
Multi-stage CI/CD with load balancingVercel deploy button
Custom email queuing systemResend (one API call)
3 containerized Docker instancesSingle Vercel deployment
Custom auth with JWT + refresh tokensSupabase Auth + GitHub OAuth
Interface-based dependency injectionDirect database calls
Red-green deployment strategiesPush to main, auto-deploy
2-week billing system integrationStripe Checkout (30 minutes)
Custom error types with metadataStandard HTTP errors
Structured logging infrastructureConsole.log (seriously)
Custom slug generation + validationSupabase database constraints

Every single "enterprise feature" I built had a dead-simple alternative for getting started. I could have validated the product in weeks, then gradually migrated the hot paths to Go as scale demanded it.

But I didn't do that because I wanted to learn. And I did learn-I now know how to build enterprise systems that can scale to millions of users. I just learned it at the cost of never shipping.

The Reality Check

The integration problem:

The core idea was simple: eliminate context switching by unifying Slack, GitHub, email, and project tools into one workspace. Focus on one project at a time.

The execution was hell.

Slack's API has rate limits. GitHub's webhook events are inconsistent. Email parsing is a nightmare. And syncing everything in real-time? Even Linear-a company with millions in funding and a world-class team-doesn't do cross-platform integration well.

I realized this before I even started building the integrations. After spending 2 months on infrastructure, I looked at the integration requirements and thought: "If Linear can't do this well, why do I think I can as a solo 15-year-old?"

The answer: I probably can't. Not as my first SaaS. Not when I need to learn marketing, sales, customer development, and viral content creation on top of the technical execution.

The skepticism:

I got 100 visitors per day to my landing page. 50 people joined the waitlist.

Then I started talking about it publicly.

The moment people learned my age, the questions started:

  • "How could a 15-year-old build enterprise software?"
  • "Is this actually production-ready or just a learning project?"
  • "Do you even understand what you're building?"

I thought the code would speak for itself. It didn't. The dev community is skeptical-rightfully so. A teenager claiming to build what venture-backed teams struggle with? It sounds like nonsense, even when it isn't.

And here's the thing: I can show them the 70,000 lines of Go. I can walk them through the architecture. But nobody wants to audit my codebase. They want to see traction. They want to see users. They want proof that the product works, not proof that I can code.

The billing breaking point:

Day 45, I started building billing. Stripe integration, subscription tiers, usage tracking, webhook handlers.

Two weeks later, I was still debugging webhook signature validation.

That's when it hit me: if billing-a solved problem with extensive documentation-takes me two weeks, how long will custom Slack sync take? How about real-time GitHub event streaming? Email parsing with thread detection?

I built the entire platform infrastructure in 2 months. Billing alone took half a month.

The math didn't work.

What I Learned

Technical lessons:

  1. Over-engineering is a tax on velocity. Every abstraction layer adds time. Every "enterprise feature" delays shipping. Go is great for scale, but you don't need scale-grade infrastructure before you have users.

  2. Use boring technology for MVPs. Supabase RLS beats custom security layers. Vercel beats custom deployments. Resend beats custom email queues. Save the interesting problems for when you're making money.

  3. Ship, then scale. I built infrastructure for millions of users before getting my first paying customer. Completely backwards. Start with Supabase and Next.js. Migrate to custom Go when you're actually hitting limits.

  4. The code is still valuable. 70k lines of production Go? That's a goldmine for future projects. When I actually need enterprise scale-when I'm serving thousands of paying customers-I'll have battle-tested patterns ready to deploy. I'm open-sourcing it because someone will find it useful when they actually need this level of infrastructure.

Marketing lessons:

  1. Code doesn't sell products. Stories do. I thought great engineering would attract users. Wrong. Nobody cares about your architecture-they care if you solve their problem. And they especially don't care if you can't explain why it matters in a 30-second video.

  2. Age is a liability until you prove otherwise. Being 15 isn't an advantage in SaaS. It's a reason for people to doubt you. Fair or not, I need to build trust before I can build a product. That means building in public, shipping fast, and showing traction.

  3. Virality beats perfection. I spent 2 months building in silence. Next time, I'm learning high-level content creation and going viral from day one. Long-form build-in-public content. Daily shipping updates. Documenting everything. I need to learn Iman Ghadzi-level video editing and hooks because attention is the bottleneck, not technical skill.

What I'm Taking With Me

The code:

This isn't wasted work. The multi-tenancy system, the testing infrastructure, the deployment pipelines-these are patterns I'll use when I actually need scale. Just not for an MVP.

I'm open-sourcing the entire codebase. Someone building a Go-based SaaS will find the service layer architecture useful. Someone learning CI/CD will appreciate the GitHub Actions setup. Someone trying to implement proper multi-tenancy will save weeks by studying the patterns.

Here's what's in there:

  • Complete multi-tenant organization system with RLS
  • Service layer with dependency injection and interfaces
  • Comprehensive testing setup with mocking
  • Production-grade error handling with custom types
  • Multi-stage deployment pipelines with load balancing
  • Custom auth flows and session management
  • Billing integration patterns with webhook handling
  • Structured logging with context propagation
  • Email notification system with async sending

All of it documented. All of it production-ready. All of it free.

The new approach:

Next time:

  • Supabase (auth + database + RLS)
  • Next.js (frontend + API routes)
  • Stripe Checkout (one plan, simple billing)
  • Vercel (deployment = push to main)
  • Resend (emails in one API call)
  • No layers. No abstractions. No "enterprise-ready" infrastructure until users demand it.

Ship in days, not months. Validate before engineering. Go viral before building.

Then, when I hit real scale-when I'm serving thousands of paying customers and Supabase starts creaking-I'll migrate the hot paths to Go. I'll add the service layers. I'll implement the dependency injection. But only when the product economics justify the engineering investment.

The vision:

Ryva's vision isn't dead. Tools like Linear are still just fancy todo apps. Developer productivity is still broken. Context switching is still a nightmare.

But the next product won't be integration hell. It'll be something simpler, more focused, and way easier to market. Something I can build in a week and iterate on daily.

And this time, I won't be building in silence.

What's Next

I'm pivoting to content-first building. Learning high-level video editing, viral hooks, and long-form storytelling. Going all-in on build-in-public.

The goal: reach 1M ARR before graduating high school. Not with this product-but with something simpler, more viral, and built in public from day one.

70,000 lines of code taught me how to build. Now I need to learn how to sell.

The irony is that I can build better than most developers twice my age. I can architect systems that scale to millions. I can write clean, maintainable, production-grade code with proper error handling, structured logging, and comprehensive testing.

But I can't get anyone to care.

So I'm learning to make people care. I'm learning to create content that spreads. I'm learning to build products that are so obviously useful, so immediately valuable, that the engineering quality becomes a bonus rather than the main story.

To Everyone Building Their First SaaS

Don't be me. Don't spend 2 months building infrastructure for users you don't have. Don't optimize for scale before you validate demand. And definitely don't build a custom billing system when Stripe Checkout exists.

Ship fast. Ship ugly. Ship often.

You can always add layers later. You can't get time back.

If you're 15 and reading this, thinking you need to build something perfect to be taken seriously: you don't. Build something useful. Build it fast. Get it in front of users. The code quality can come later, after you've proven the idea works.

And if you're building enterprise SaaS and actually need infrastructure that scales, the entire codebase will be open-sourced soon. 70k lines of production Go, modular architecture, CI/CD pipelines, and comprehensive docs. Learn from my mistakes. Use the patterns that work. Skip the ones that don't.

The codebase will be on GitHub soon. Follow my journey as I rebuild, this time the right way. I'm focusing on building in public, learning viral content creation, and shipping an MVP in one week instead of two months. Still figuring out the next product, but I can promise you this: it won't take 2 months to validate.