Back to Blog

Building an All-in-One Business Platform: Lessons from MindHyv

Building an All-in-One Business Platform: Lessons from MindHyv

MindHyv is the most ambitious project we have built at Threshline. It is an all-in-one business platform for entrepreneurs that combines a social network, a booking system, an invoicing engine, and a storefront into a single product. Think of it as if LinkedIn, Calendly, Stripe Billing, and Shopify had a child — and that child was designed for solopreneurs and small business owners who are tired of juggling six different subscriptions.

This post covers the technical lessons we learned building it. Not the marketing story or the product vision — the actual engineering decisions, architecture patterns, and hard tradeoffs that went into shipping a multi-feature SaaS platform with a four-person team.

The Core Problem: Feature Sprawl in a Single Product

The challenge with an all-in-one platform is that each feature module — social, booking, invoicing, selling — is complex enough to be its own standalone product. Companies have raised millions to build just a booking system. Others have spent years on just an invoicing tool.

We had to build all four and make them feel like one product.

The naive approach is to build four separate apps and stitch them together with navigation. The result is something that feels like four separate apps stitched together with navigation. Users notice. They feel the seams.

Our approach was to identify the shared primitives across all modules and build those first:

  • User profiles are the universal entity. Your social profile is your booking page is your storefront is your invoice sender.
  • Contacts are shared across modules. A follower on your social feed can book an appointment, buy a product, and receive an invoice — all tracked as one relationship.
  • Notifications are unified. Whether someone likes your post, books a slot, pays an invoice, or purchases a product, it flows through the same notification system.
  • The activity feed is the connective tissue. Business activity from all modules surfaces in the feed, creating a living timeline of your business.

This shared-primitive approach meant we spent the first three weeks building infrastructure that had nothing to do with any specific feature. It felt slow. It was the best decision we made.

A clean all-in-one digital workspace with multiple tools on screen

Database Architecture: Shared Core, Module Schemas

We use Supabase (PostgreSQL) as our database. The schema is organized around a shared core with module-specific extensions.

The core tables look like this:

-- Core identity
create table profiles (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade,
  display_name text not null,
  handle text unique not null,
  avatar_url text,
  bio text,
  business_type text,
  created_at timestamptz default now()
);

-- Universal contact/relationship
create table connections (
  id uuid primary key default gen_random_uuid(),
  follower_id uuid references profiles(id) on delete cascade,
  following_id uuid references profiles(id) on delete cascade,
  status text default 'active', -- active, blocked, muted
  source text, -- social, booking, purchase, manual
  created_at timestamptz default now(),
  unique(follower_id, following_id)
);

-- Unified notification
create table notifications (
  id uuid primary key default gen_random_uuid(),
  recipient_id uuid references profiles(id) on delete cascade,
  actor_id uuid references profiles(id),
  type text not null, -- like, comment, booking, payment, purchase
  module text not null, -- social, booking, invoicing, store
  reference_id uuid, -- polymorphic reference to the source entity
  reference_type text, -- post, appointment, invoice, order
  read boolean default false,
  created_at timestamptz default now()
);

Each module then extends this core. The booking module has its own tables for availability slots, appointments, and calendar syncs. The invoicing module has tables for invoices, line items, and payment records. The store module has products, orders, and inventory. But they all reference back to profiles and connections.

This approach gives us two things: data consistency (a contact is always a contact, regardless of which module created the relationship) and query efficiency (we can answer questions like “show me all activity related to this contact” with straightforward joins instead of cross-service API calls).

We covered our broader approach to Supabase architecture in our post on building real-time features with Supabase.

The Shared Design System

When you have four feature modules that need to feel like one product, the design system is not optional — it is structural.

We built a component library in SvelteKit that enforces visual consistency across modules. The key patterns:

Module-agnostic layouts. Every module uses the same page shell, the same sidebar navigation, the same header pattern. The user never feels like they have “left” one module and “entered” another.

Shared interaction patterns. Creating a post, creating a booking slot, creating an invoice, and creating a product all use the same multi-step form pattern with the same validation behavior and the same success state.

A unified color system with module accents. The base palette is consistent, but each module has a subtle accent color that provides wayfinding without breaking visual cohesion.

// Design tokens shared across all modules
export const tokens = {
  colors: {
    primary: 'hsl(230, 65%, 52%)',
    surface: 'hsl(220, 20%, 98%)',
    text: 'hsl(220, 25%, 12%)',
    border: 'hsl(220, 15%, 90%)',
    // Module accents — subtle, used for icons and active states
    modules: {
      social: 'hsl(210, 70%, 55%)',
      booking: 'hsl(150, 60%, 42%)',
      invoicing: 'hsl(35, 85%, 52%)',
      store: 'hsl(280, 55%, 55%)',
    },
  },
  spacing: {
    xs: '0.25rem',
    sm: '0.5rem',
    md: '1rem',
    lg: '1.5rem',
    xl: '2rem',
    '2xl': '3rem',
  },
  radius: {
    sm: '0.375rem',
    md: '0.5rem',
    lg: '0.75rem',
    full: '9999px',
  },
} as const;

We wrote more about our approach to design systems in our post on building a design system from scratch. The MindHyv system is the most mature version of our process.

Professionals networking and connecting through a social business platform

Real-Time Social Features

The social module in MindHyv is not a bolted-on afterthought. It is the primary surface users interact with when they open the app. The activity feed shows posts from people they follow, business updates (new products, availability changes, completed bookings), and engagement activity.

Building a real-time social feed on Supabase requires careful thinking about subscriptions and data flow:

// Real-time feed subscription
import { supabase } from '$lib/supabase';

function subscribeFeed(profileId: string, onNewPost: (post: Post) => void) {
  return supabase
    .channel(`feed:${profileId}`)
    .on(
      'postgres_changes',
      {
        event: 'INSERT',
        schema: 'public',
        table: 'feed_items',
        filter: `recipient_id=eq.${profileId}`,
      },
      (payload) => {
        onNewPost(payload.new as Post);
      }
    )
    .subscribe();
}

The feed_items table is a materialized fan-out of posts. When a user creates a post, a database function writes a feed_item row for each of their followers. This is the fan-out-on-write pattern — it is write-heavy but read-cheap, which is the right tradeoff for a feed where reads outnumber writes by 100:1.

We have a dedicated post covering social feature architecture: how to build a social network feature set for your SaaS. It goes deeper into the feed, following, messaging, and notification patterns.

Booking System: Harder Than It Looks

Booking systems are deceptively complex. The core concept — someone picks a time slot and books it — sounds simple until you account for:

  • Timezones. The business owner is in Manila, the client is in New York. The slot needs to display correctly for both.
  • Conflict detection. Two people clicking “book” on the same slot at the same time.
  • Buffer times. A 30-minute appointment might need 15 minutes of buffer before and after.
  • Recurring availability. The business is open 9 AM to 5 PM on weekdays, except holidays.
  • Calendar sync. The booking should appear in Google Calendar and block the time.

We solved timezone normalization by storing everything in UTC and converting at the presentation layer:

import { formatInTimeZone } from 'date-fns-tz';

function displaySlot(slot: AvailabilitySlot, userTimezone: string): string {
  return formatInTimeZone(
    new Date(slot.start_time),
    userTimezone,
    'EEEE, MMM d, yyyy h:mm a zzz'
  );
}

function createBooking(slotId: string, clientTimezone: string) {
  // All times stored in UTC — timezone is metadata, not data
  return supabase.rpc('book_appointment', {
    p_slot_id: slotId,
    p_client_timezone: clientTimezone,
  });
}

Conflict detection uses PostgreSQL advisory locks to prevent double-booking:

create or replace function book_appointment(
  p_slot_id uuid,
  p_client_timezone text
) returns uuid as $$
declare
  v_appointment_id uuid;
  v_slot record;
begin
  -- Advisory lock on the slot to prevent race conditions
  perform pg_advisory_xact_lock(hashtext(p_slot_id::text));

  select * into v_slot from availability_slots
  where id = p_slot_id and status = 'available'
  for update;

  if v_slot is null then
    raise exception 'Slot is no longer available';
  end if;

  -- Create the appointment
  insert into appointments (slot_id, client_id, client_timezone, status)
  values (p_slot_id, auth.uid(), p_client_timezone, 'confirmed')
  returning id into v_appointment_id;

  -- Mark slot as booked
  update availability_slots set status = 'booked' where id = p_slot_id;

  return v_appointment_id;
end;
$$ language plpgsql security definer;

We wrote a full deep dive on booking architecture in our post on booking and scheduling systems.

An entrepreneur managing their business tools and workspace on a laptop

Invoicing: The Boring Critical Feature

Nobody gets excited about invoicing. But for MindHyv’s target users — solopreneurs and small business owners — getting paid is the most important feature. The invoicing module needed to be dead simple to use while handling real-world complexity: tax calculations, partial payments, recurring invoices, and payment tracking.

The key architectural decision was treating invoices as immutable documents. Once an invoice is sent, the original is never modified. Adjustments create new linked documents (credit notes, revised invoices). This matches accounting standards and gives users a clear audit trail.

type InvoiceStatus =
  | 'draft'      // Editable, not yet sent
  | 'sent'       // Delivered to client, awaiting payment
  | 'viewed'     // Client opened the invoice
  | 'partial'    // Some payment received
  | 'paid'       // Fully paid
  | 'overdue'    // Past due date, not fully paid
  | 'void';      // Cancelled with credit note

// State transitions are enforced at the database level
// draft -> sent -> viewed -> partial -> paid
// sent/viewed/partial/overdue -> void (creates credit note)

The storefront module follows a similar pattern with immutable order records and clear state machines.

What We Would Do Differently

Five things we would change if we started MindHyv over:

  1. Start with the notification system, not the feed. Notifications are the connective tissue across modules. We built them third and had to retrofit cross-module notification support.

  2. Use database enums instead of text for status fields. We started with text columns for flexibility. The flexibility was not worth the lack of type safety. PostgreSQL enums catch invalid states at the database level.

  3. Build the shared contact model before any module. We initially had separate “follower” and “client” concepts that we later unified. The unification was painful.

  4. Invest in end-to-end tests earlier. With four modules interacting, integration bugs were our biggest source of regressions. Unit tests did not catch them. E2E tests with Playwright would have.

  5. Design the mobile experience first. MindHyv’s users are entrepreneurs on the go. We built desktop-first and adapted to mobile. The mobile experience would have been better if we had designed for it from the start.

The All-in-One Tradeoff

Building an all-in-one platform means you are competing with best-of-breed tools in every category. MindHyv’s booking system will never have as many features as Calendly. Its invoicing will never match FreshBooks. Its storefront will never rival Shopify.

That is fine. The value is not in any single module — it is in the integration between them. When a client books an appointment, the business owner can send them an invoice from the same platform, and that client can also follow the business’s social feed and browse their products. No Zapier glue. No data silos. One relationship, one platform.

That integration is the product. And it is something that no combination of point solutions can replicate without significant duct tape.

Building a multi-feature platform is the hardest type of software project we have taken on. It requires discipline in architecture, consistency in design, and ruthless prioritization of what to build and what to defer. But when it works — when a user can run their entire business from one tab — the result is worth the complexity.

If you are building a platform that combines multiple feature domains and want to learn from what we shipped, reach out at hello@threshline.com. We have been through the hard parts and can help you avoid the mistakes we made.