Back to Blog

Lessons From Shipping 12 Products in 5 Years

Lessons From Shipping 12 Products in 5 Years

Five years and twelve shipped products. Some were MVPs that launched in weeks. Others were complex platforms that took months of iteration. A few pivoted mid-build. One got scrapped entirely.

Through all of it, patterns emerged. Not the kind you read about in blog posts (ironic, we know) — the kind you only learn by watching the same mistake repeat across different clients, different industries, and different tech stacks.

Here is what stuck.

Scope Always Grows — Plan for It

Every single product we have shipped grew in scope. Every one. It does not matter how disciplined the client is, how tight the spec was, or how many times we said “let’s keep it simple.” Scope grows because understanding grows. You start building a freelancer management platform and realize halfway through that the invoicing module needs recurring billing. You start building a feedback platform and discover that customers need Slack integration on day one, not day thirty.

The mistake is not scope growth itself — it is pretending it will not happen. We stopped fighting it and started budgeting for it. Every project now gets a 20-30% buffer in both time and budget. Not as padding for laziness, but as room for the discoveries that always come.

The practical version of this: we break every project into two-week milestones and re-scope at each checkpoint. The total scope can grow, but the next two weeks stay fixed.

Reaching a milestone after successfully shipping a product

Design Before Code Saves More Time Than You Think

We used to jump into code fast. We are engineers. We like building things. But we learned the hard way — on multiple projects — that spending a week on design and information architecture saves three weeks of rework later.

When we built MindHyv, the all-in-one business platform, we spent two full weeks on wireframes and user flows before writing a single line of code. It felt slow at the time. But that upfront investment meant the data model was right from the start. The navigation structure made sense. The feature boundaries were clear.

Compare that to an earlier project where we started coding on day three. We rebuilt the navigation twice, migrated the database schema four times, and threw away an entire module because the user flow did not match how people actually used the product.

Design does not mean pixel-perfect mockups in Figma. It means:

  • A clear information architecture
  • User flows for every core action
  • A data model that accounts for relationships
  • A rough component hierarchy

That work prevents the most expensive kind of rework — structural rework.

Deploy Early, Deploy Often

Our first deploys used to happen weeks into a project. Now they happen on day one. Literally day one. Even if it is just a landing page with authentication, it goes to a real URL on a real server.

Why? Because deployment problems compound. The longer you wait to deploy, the more differences accumulate between your local environment and production. Database migrations stack up. Environment variables diverge. Third-party service configurations drift.

When we built Just The Rip, the digital pack-opening platform, we had a working deploy on Vercel within the first day. It did nothing interesting — just showed a logo and a “coming soon” message. But it meant that every feature we built after that was tested in production conditions from the start. The WebSocket connections for real-time pack openings? Tested on real infrastructure from week one. The payment processing? Connected to Stripe’s test mode on a real domain, not localhost.

Our standard setup for new projects:

// deploy.config.ts — our typical Vercel/Cloudflare setup
export const config = {
  production: {
    branch: 'main',
    domain: 'app.clientdomain.com',
    env: 'production',
  },
  staging: {
    branch: 'develop',
    domain: 'staging.clientdomain.com',
    env: 'staging',
  },
  preview: {
    branch: '*',
    domain: 'pr-*.clientdomain.com',
    env: 'preview',
  },
};

Three environments from day one. Production, staging, and preview deploys for every pull request. It sounds like overkill for a new project, but it saves hours of debugging later.

Monitoring Is Non-Negotiable

This one hurt to learn. We shipped a product, the client was happy, and we moved on. Two weeks later, the database was running out of connections because a query was missing a connection pool limit. Nobody noticed until users started seeing errors.

Now, monitoring goes in before features. Not after launch. Not “when we have time.” Before features.

The minimum monitoring stack we set up on every project:

// Basic health check endpoint — goes in on day one
export async function GET() {
  try {
    const dbCheck = await db.query('SELECT 1');
    const cacheCheck = await redis.ping();

    return new Response(
      JSON.stringify({
        status: 'healthy',
        database: dbCheck ? 'connected' : 'disconnected',
        cache: cacheCheck === 'PONG' ? 'connected' : 'disconnected',
        timestamp: new Date().toISOString(),
      }),
      { status: 200 }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ status: 'unhealthy', error: error.message }),
      { status: 503 }
    );
  }
}

Beyond health checks, we set up error tracking (Sentry), uptime monitoring, and basic database query performance logging. It takes half a day to configure. It has saved us from disasters more times than we can count.

When we built Spots Mexico, the photography location directory, monitoring caught a slow geospatial query early — before the location database grew large enough for it to become a real problem. We optimized it in an afternoon instead of firefighting it during a traffic spike.

Software deployment pipeline running on a development screen

Simple Beats Clever, Every Single Time

This is the lesson we keep re-learning. Clever architectures, clever abstractions, clever patterns — they all feel good when you write them. They feel terrible six months later when you need to change something and cannot remember what the clever thing does.

We went through a phase where we over-abstracted everything. Generic CRUD factories. Dynamic form generators. Polymorphic database schemas. Every one of those “saved time” in the short term and cost time in the long term.

The turning point was a project where we built a generic notification system that could handle email, SMS, push, and in-app notifications through a single abstract interface. It was elegant. It was also impossible to debug when SMS notifications stopped arriving because the abstraction hid the specific SMS provider logic behind three layers of indirection.

Now we follow a rule: do not abstract until the third use. The first time you need something, write it directly. The second time, notice the duplication but leave it. The third time, extract the abstraction — because now you actually understand the shape of the problem.

// Instead of this:
class NotificationFactory {
  create(type: NotificationType): AbstractNotification { ... }
}

// We write this:
async function sendEmailNotification(to: string, subject: string, body: string) {
  await resend.emails.send({ from: '[email protected]', to, subject, html: body });
}

async function sendSMSNotification(phone: string, message: string) {
  await twilio.messages.create({ to: phone, body: message, from: SMS_FROM });
}

Two functions. No abstraction. Easy to understand. Easy to debug. Easy to replace one without touching the other.

The Client Knows Their Users Better Than You Do

We are good at building software. We are not good at understanding someone else’s market. Early on, we would push back on client decisions that seemed wrong to us — unnecessary features, weird UX flows, things that “nobody would want.”

We were wrong more often than we were right.

When the Vincelio team told us that LATAM influencers needed a specific contract workflow that seemed overly complex to us, we pushed back. They insisted. They were right. That workflow matched how brands and influencers actually negotiate in that market. Our “cleaner” alternative would have missed the mark entirely.

Now, our role is to push back on technical complexity, not business decisions. If a client says their users need something, we trust that and figure out the best way to build it. If we see a technical risk or a simpler way to achieve the same outcome, we raise it. But we stopped second-guessing domain expertise.

The Tech Stack Matters Less Than You Think

We have built products with SvelteKit, Astro, Flutter, React, and various backend technologies. The tech stack has never been the reason a product succeeded or failed.

What matters is:

  • How fast you can iterate. Can you ship a change in hours, not days?
  • How well you know the stack. A team that knows Rails inside out will outperform a team learning Next.js, regardless of which framework is “better.”
  • How maintainable the result is. Will the client’s future team be able to work in this codebase?

We chose SvelteKit and Supabase as our primary stack not because they are the best tools in some objective sense, but because our team knows them deeply, they let us move fast, and they produce maintainable codebases. That is the whole calculation.

For more on our stack decisions, see our post on why we use Supabase and our startup tech stack guide.

Documentation Is a Gift to Your Future Self

We document everything now. Not because we love writing documentation — nobody does — but because we have been burned by the alternative.

Every project gets:

  • A README that explains how to run it locally
  • An architecture decision record for any non-obvious choice
  • Inline comments for business logic (not for obvious code)
  • API documentation generated from the schema

The architecture decision records are the most valuable. When a future developer asks “why is this done this way?” the ADR answers it. Without ADRs, the answer is usually “nobody remembers” followed by three hours of git archaeology.

What Still Surprises Us

After twelve products, you would think nothing surprises us. But two things consistently do.

First, how much impact small UX details have. A loading skeleton instead of a spinner. An optimistic update instead of a loading state. A confirmation message that says exactly what happened instead of a generic “Success!” These small things are what users actually notice and remember.

Second, how different every project is despite following similar patterns. The technical challenges of building MindHyv — a platform combining social features, booking, invoicing, and selling — were completely different from building Trackelio, even though both are SaaS platforms with multi-tenant architectures. The domain shapes the architecture more than the architecture shapes the domain.

Team celebrating a project achievement together

The Compounding Lesson

The real meta-lesson from five years of shipping is that good habits compound. Deploying on day one is not just about catching deployment issues — it creates a culture of continuous delivery that makes everything faster. Writing documentation is not just about onboarding — it forces you to think clearly about your decisions. Monitoring is not just about uptime — it gives you confidence to ship quickly because you will know immediately if something breaks.

None of these lessons are revolutionary. Most of them are common advice you have heard before. The difference is that we have lived every one of them across a dozen products, and we can tell you exactly which project taught us each lesson the hard way.

If you are building something and want a team that has already made these mistakes (so you do not have to), reach out at [email protected].