Back to Blog
March 26, 2026

Build Social Proof That Converts: Trust Signals for Indie Hackers

Your landing page looks great — but visitors bounce because nobody else seems to be using it. Fix that in under two hours.

Social proof components on a landing page — activity toasts, testimonial carousel, and visitor count badge

Why Does Social Proof Convert So Well for Indie Projects?

Social proof triggers herd behavior — visitors trust your product more when they see others already using it, boosting conversions by up to 15% with simple trust signals.

You built the thing. The landing page is pixel-perfect. But visitors show up, scroll for three seconds, and leave. The problem isn't your product — it's that your page feels empty. No activity. No proof anyone else cares.

Social proof fixes that. According to a 2025 Baymard Institute study, adding trust signals to product pages increases conversion rates by 12-17% on average. For indie projects with zero brand recognition, that number can be even higher.

And here's the best part: you don't need a marketing team or expensive tools. Three components — an activity toast, a testimonial carousel, and a visitor badge — are enough to transform a dead-looking page into one that feels alive. Let's build all three in under two hours.

Time Budget

  • Activity Toast: ~20 minutes
  • Testimonial Carousel: ~30 minutes
  • Visitor Count Badge: ~25 minutes
  • Integration & polish: ~15 minutes
  • Total: ~90 minutes

Component 1: Real-Time Activity Toast (20 min)

You know those little popups that say "Sarah from Austin just signed up"? They work ridiculously well. A 2024 Nosto report found that real-time social proof notifications increase add-to-cart rates by 10-15%. Let's build one with Supabase realtime and zero external libraries.

First, set up a Supabase project (free tier is plenty). Create a table to store activity events:

-- Supabase SQL Editor
create table public.activity_events (
  id uuid default gen_random_uuid() primary key,
  event_type text not null default 'signup',
  user_name text not null,
  user_location text,
  created_at timestamptz default now()
);

-- Enable realtime on this table
alter publication supabase_realtime add table public.activity_events;

-- Insert a test event
insert into public.activity_events (user_name, user_location)
values ('Alex', 'Barcelona');

Now the React component. This subscribes to new inserts and shows a sliding toast:

"use client";

import { useEffect, useState } from "react";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

interface ActivityEvent {
  user_name: string;
  user_location: string;
  event_type: string;
}

export function ActivityToast() {
  const [event, setEvent] = useState<ActivityEvent | null>(null);
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const channel = supabase
      .channel("activity")
      .on(
        "postgres_changes",
        { event: "INSERT", schema: "public", table: "activity_events" },
        (payload) => {
          setEvent(payload.new as ActivityEvent);
          setVisible(true);
          setTimeout(() => setVisible(false), 4000);
        }
      )
      .subscribe();

    return () => { supabase.removeChannel(channel); };
  }, []);

  if (!visible || !event) return null;

  return (
    <div className="fixed bottom-4 left-4 animate-slide-up rounded-lg
      bg-white px-4 py-3 shadow-lg transition-all">
      <p className="text-sm text-gray-800">
        <strong>{event.user_name}</strong> from {event.user_location}
        {" "}just signed up
      </p>
    </div>
  );
}

Add a simple slide-up animation to your Tailwind config and you're done. The toast appears for four seconds whenever someone signs up, then fades away. No polling, no WebSocket boilerplate — Supabase handles the connection.

Component 2: Testimonial Carousel (30 min)

Testimonials are the oldest social proof trick in the book, but most indie hackers do them wrong. A wall of text quotes that nobody reads. Instead, build a smooth auto-scrolling carousel with avatars and real names. Here's the key: no JavaScript animation library needed. Pure CSS does it better.

interface Testimonial {
  name: string;
  role: string;
  avatar: string;
  quote: string;
}

const testimonials: Testimonial[] = [
  {
    name: "Maria Garcia",
    role: "Founder, ShipFast.es",
    avatar: "/avatars/maria.jpg",
    quote: "Went from zero social proof to 40% more signups in a week.",
  },
  {
    name: "Carlos Ruiz",
    role: "Indie Hacker, Valencia",
    avatar: "/avatars/carlos.jpg",
    quote: "The activity toast alone doubled my trial conversions.",
  },
];

export function TestimonialCarousel({ items }: { items: Testimonial[] }) {
  return (
    <div className="overflow-hidden">
      <div className="flex animate-scroll gap-6">
        {[...items, ...items].map((t, i) => (
          <div key={i} className="min-w-[300px] rounded-xl bg-white/5 p-6">
            <p className="text-white/80">"{t.quote}"</p>
            <div className="mt-4 flex items-center gap-3">
              <img src={t.avatar} alt={t.name}
                className="h-10 w-10 rounded-full" />
              <div>
                <p className="font-semibold text-white">{t.name}</p>
                <p className="text-sm text-white/60">{t.role}</p>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

The trick is duplicating the array and using a CSS @keyframes scroll animation that moves the container by exactly 50%. When it loops, the duplicate items create a seamless infinite scroll. Zero JavaScript, zero jank.

Component 3: Visitor Count Badge (25 min)

A simple "127 people are viewing this page" badge creates urgency. Even if your numbers are modest, showing real-time activity makes your page feel alive. Connect it to Supabase presence for accurate counts:

"use client";

import { useEffect, useState } from "react";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

export function VisitorBadge() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const channel = supabase.channel("visitors", {
      config: { presence: { key: crypto.randomUUID() } },
    });

    channel
      .on("presence", { event: "sync" }, () => {
        setCount(Object.keys(channel.presenceState()).length);
      })
      .subscribe(async (status) => {
        if (status === "SUBSCRIBED") {
          await channel.track({ online_at: new Date().toISOString() });
        }
      });

    return () => { supabase.removeChannel(channel); };
  }, []);

  if (count < 2) return null;

  return (
    <div className="flex items-center gap-2 rounded-full bg-green-500/10
      px-3 py-1 text-sm text-green-400">
      <span className="h-2 w-2 animate-pulse rounded-full bg-green-400" />
      {count} people viewing this page
    </div>
  );
}

The badge only appears when at least two people are on the page — showing "1 person viewing" would look sad. Supabase presence handles all the tracking, and the count updates in real time as visitors arrive and leave.

Putting It All Together

Drop all three components into your landing page layout. The activity toast goes at the bottom-left, the testimonial carousel sits below your hero section, and the visitor badge lives in your navbar or above the fold.

Implementation Checklist

  • Create Supabase project and enable realtime
  • Add environment variables for Supabase URL and anon key
  • Build ActivityToast component with realtime subscription
  • Build TestimonialCarousel with CSS-only infinite scroll
  • Build VisitorBadge with Supabase presence
  • Add slide-up and scroll animations to Tailwind config
  • Test with multiple browser tabs to verify realtime updates

The entire setup runs on Supabase's free tier. No monthly costs until you're well past the point where social proof matters — by then, you'll have real traction to show for it.

Common Mistakes to Avoid

Don't fake it. Fabricated testimonials and inflated numbers destroy trust faster than no social proof at all. Start with real beta tester feedback, even if you only have three quotes. Authenticity beats volume every time.

Don't overdo animations. One toast every 30 seconds is fine. One every 3 seconds is spam. Throttle your activity notifications so they feel natural, not desperate.

Don't show zeros. If nobody is online, hide the visitor badge. If you have no recent signups, disable the toast. Social proof only works when there's actually something to prove.

The Bottom Line

Social proof isn't about tricking visitors — it's about showing them what's already true. People are using your product. People trust it. Make that visible, and your conversion rate will follow. Three components, ninety minutes, zero monthly cost. Ship it today.

Ready to ship faster?

Desplega.ai helps indie hackers and solopreneurs ship quality apps faster with AI-powered development and testing tools.

Start Shipping Today

Frequently Asked Questions

Do I need a backend to add social proof to my landing page?

Not necessarily. Static testimonials need zero backend. For real-time toasts and visitor counts, Supabase free tier handles it with minimal setup and no server management.

How many signups do I need before social proof looks credible?

Start showing activity toasts after 10-20 real signups. Before that, use testimonials from beta testers or show GitHub stars and Twitter followers as alternative proof.

Will social proof components slow down my page load speed?

Not with this approach. The testimonial carousel is pure CSS. Activity toasts lazy-load via Supabase realtime after hydration, adding zero blocking time to initial paint.

Can I use these components with frameworks other than Next.js?

Yes. The testimonial carousel is vanilla React. The Supabase realtime hooks work in any React app — Remix, Vite, Astro with React islands. Just swap the imports.

Is Supabase realtime free enough for a side project?

The free tier supports 200 concurrent realtime connections and 500MB database storage. That handles thousands of daily visitors easily — more than enough for early traction.