Building TooSimple: A Dead Simple Testimonials Platform at 1/3 the Cost

February 19, 2026 (4mo ago)

Every bootstrapped founder knows the struggle: you need social proof on your website, but the existing testimonial platforms charge $29-99/month for what should be a simple feature. Collect a quote, display it nicely, done. That's why I built toosimple.io - testimonial collection that's dead simple, at 1/3 the cost.

The Problem with Existing Testimonial Tools

The testimonial SaaS market is dominated by a few players charging premium prices for features most founders don't need:

  1. Overpriced - $29-99/month for collecting and displaying customer quotes
  2. Feature bloat - Complex dashboards, analytics suites, and integrations you'll never use
  3. Vendor lock-in - Proprietary embed scripts that break when you cancel
  4. No video support - Or video behind the highest tier

What bootstrapped founders actually need:

  • Collect text and video testimonials from customers
  • Import existing testimonials from social platforms
  • Embed beautiful widgets on any website
  • Simple dashboard to approve/reject
  • Affordable pricing that doesn't scale with success

That's exactly what toosimple.io delivers - all of this for $9/month with unlimited testimonials and no branding.

The Tech Stack

I built TooSimple as a production-grade Bun monorepo using Turborepo, with separate apps for the marketing site and authenticated dashboard:

Monorepo Architecture

  • Turborepo for build orchestration with caching
  • Bun 1.0+ as runtime and package manager
  • @v1/app - Dashboard (testimonial management, widget builder, analytics)
  • @v1/web - Marketing website (landing, pricing, public pages)
  • @v1/api - Supabase database and migrations

Frontend & UI

  • Next.js 16 with App Router and React 19
  • TypeScript in strict mode
  • Tailwind CSS v4 with Shadcn/UI components
  • Framer Motion for smooth animations
  • Recharts for analytics dashboards
  • React Hook Form + Zod for form validation

Backend & Database

  • Supabase for PostgreSQL hosting
  • Drizzle ORM for type-safe queries and migrations
  • Better Auth for authentication (email/password + social OAuth)
  • Upstash Redis for rate limiting and caching

Storage & Media

  • Cloudflare R2 for video and logo storage (zero egress costs)
  • FFmpeg for client-side video transcoding
  • Trigger.dev v4 for background video processing

Email & Payments

  • React Email + Resend for 14+ transactional email templates
  • Polar.sh as merchant of record for subscriptions
  • Sentry for error tracking with source maps

Want to see this in action? Try toosimple.io and create your first collection form in under 2 minutes. No credit card required.

Architecture Decisions

1. Zero-Dependency Widget Embed System

The most technically interesting part of TooSimple is the embeddable widget. Customers paste a 2-line snippet on their website, and testimonials appear beautifully:

<script src="https://toosimple.io/widget.js"></script>
<div data-toosimple-widget="abc123"></div>

The widget JavaScript is completely self-contained - zero external dependencies:

// Widget initialization with batch loading
const BATCH_SIZE = 3;
const BATCH_DELAY = 100; // ms between batches
 
class TooSimpleWidget {
  private static inflight = new WeakMap();
 
  static async init(container: HTMLElement) {
    const publicId = container.dataset.toosimpleWidget;
 
    // Request deduplication - prevent race conditions
    if (this.inflight.has(container)) {
      return this.inflight.get(container);
    }
 
    const promise = this.fetchAndRender(publicId, container);
    this.inflight.set(container, promise);
 
    return promise;
  }
 
  private static async fetchAndRender(id: string, el: HTMLElement) {
    // Retry with exponential backoff
    for (let attempt = 0; attempt < 3; attempt++) {
      try {
        const res = await fetch(`/api/widget/${id}`, {
          signal: AbortSignal.timeout(10000)
        });
        const data = await res.json();
        this.render(data, el);
        return;
      } catch {
        await new Promise(r =>
          setTimeout(r, Math.pow(2, attempt) * 1000)
        );
      }
    }
  }
}

Why this matters:

  • Works everywhere - No React, no framework dependency, just vanilla JS
  • Batch loading - 3 widgets per 100ms prevents API overload on pages with multiple widgets
  • Request deduplication - WeakMap prevents duplicate fetches for the same widget
  • Timeout protection - 10-second abort signal prevents hanging requests
  • XSS prevention - Content sanitization catches malicious testimonial text
  • Memory management - Event listeners tracked and cleaned up properly

2. Four Widget Layouts

TooSimple ships with 4 display layouts, each lazy-loaded to minimize bundle size:

// Layout components loaded on demand
const LAYOUTS = {
  carousel: () => import('./layouts/carousel'),
  grid: () => import('./layouts/grid'),
  single: () => import('./layouts/single'),
  'wall-of-love': () => import('./layouts/wall-of-love')
};

Carousel - Rotating testimonials with autoplay, arrows, pagination dots, and touch/swipe support on mobile.

Grid - Responsive grid with customizable column counts that adapts to container width.

Single - Display one featured testimonial at a time, perfect for hero sections.

Wall of Love - Masonry-style layout that creates a dense, scrollable wall of social proof.

Each layout is fully customizable: theme (light/dark/auto), colors, spacing, padding, border radius, shadows, and which elements to show (avatar, rating, company, date).

3. Video Testimonial Pipeline

Video testimonials are the highest-converting social proof, but processing video is hard. I built a dual-processing pipeline:

// packages/jobs/trigger/video-processing.ts
export const processVideo = task({
  id: "process-video",
  retry: { maxAttempts: 3 },
  run: async ({ videoId, uploadUrl }) => {
    // Step 1: Download from R2
    const video = await downloadFromR2(uploadUrl);
 
    // Step 2: Transcode to 720p with FFmpeg
    const processed = await ffmpeg(video, {
      resolution: '720p',
      maxDuration: 90, // 90-second cap
      format: 'mp4',
      codec: 'h264'
    });
 
    // Step 3: Generate thumbnail
    const thumbnail = await extractFrame(processed, { at: '00:00:02' });
 
    // Step 4: Upload processed files to R2
    await uploadToR2(processed, `videos/${videoId}/processed.mp4`);
    await uploadToR2(thumbnail, `videos/${videoId}/thumb.jpg`);
 
    // Step 5: Update testimonial status
    await db.update(testimonials)
      .set({
        processingStatus: 'completed',
        mediaUrl: processed.url,
        thumbnailUrl: thumbnail.url
      })
      .where(eq(testimonials.id, videoId));
 
    // Step 6: Notify project owner
    await sendVideoReadyEmail(videoId);
  }
});

Client-side preview uses FFmpeg in the browser for immediate feedback, while Trigger.dev handles production-quality processing in the background. Users see their video immediately, and it gets optimized asynchronously.

4. Social Platform Import System

Manually collecting testimonials is tedious. TooSimple imports from 12+ platforms automatically:

// Supported import sources
type ImportSource =
  | 'twitter'    | 'linkedin'   | 'reddit'
  | 'youtube'    | 'google'     | 'producthunt'
  | 'trustpilot' | 'capterra'   | 'facebook'
  | 'instagram'  | 'yelp'       | 'appstore';
 
// Each import extracts platform-specific metadata
interface ImportedTestimonial {
  source: ImportSource;
  externalId: string;
  externalUrl: string;
  authorName: string;
  authorAvatar?: string;
  authorHandle?: string;
  authorVerified: boolean;
  content: string;
  rating?: number;
  platformMetadata: Record<string, unknown>; // Likes, retweets, etc.
  contentHash: string; // Duplicate detection
}

Duplicate detection uses content hashing to prevent importing the same testimonial twice. Each platform stores its own metadata (Twitter metrics, LinkedIn reactions, etc.) for rich display.

5. Auto-Approval with Configurable Rules

For high-volume collection, manual approval becomes a bottleneck. I built a rule engine that auto-approves testimonials based on configurable criteria:

// Project-level auto-approval settings (stored as JSON)
interface AutoApprovalRules {
  enabled: boolean;
  minimumRating: number;        // e.g., 4+ stars auto-approve
  requireContent: boolean;       // Must have text, not just rating
  profanityFilter: boolean;      // Block profanity
  minimumContentLength: number;  // e.g., 20+ characters
}
 
async function evaluateTestimonial(
  testimonial: NewTestimonial,
  rules: AutoApprovalRules
): Promise<'approved' | 'pending'> {
  if (!rules.enabled) return 'pending';
 
  if (rules.minimumRating && testimonial.rating < rules.minimumRating)
    return 'pending';
 
  if (rules.requireContent && !testimonial.content?.trim())
    return 'pending';
 
  if (rules.profanityFilter && containsProfanity(testimonial.content))
    return 'pending';
 
  if (testimonial.content.length < rules.minimumContentLength)
    return 'pending';
 
  return 'approved';
}

This lets users set it and forget it - quality testimonials flow straight to their widgets while edge cases wait for review.

6. Cloudflare R2 for Zero-Egress Storage

Video testimonials and logos need reliable, fast storage. I chose Cloudflare R2 over S3 for one reason: zero egress costs.

// Presigned URL generation for secure uploads
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
 
const r2 = new S3Client({
  region: 'auto',
  endpoint: process.env.R2_ENDPOINT,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY!,
    secretAccessKey: process.env.R2_SECRET_KEY!,
  },
});
 
export async function getPresignedUploadUrl(
  userId: string,
  fileName: string
) {
  const key = `${userId}/${crypto.randomUUID()}-${fileName}`;
 
  const url = await getSignedUrl(r2, new PutObjectCommand({
    Bucket: process.env.R2_BUCKET,
    Key: key,
    ContentType: 'video/mp4',
  }), { expiresIn: 3600 });
 
  return { url, key };
}

Why R2 over S3:

  • Zero egress fees - Widget embeds serve videos globally without bandwidth costs
  • S3-compatible API - Drop-in replacement, same SDK
  • Global CDN - Cloudflare's network means fast delivery worldwide
  • User isolation - Each user's files are namespaced by userId for security

7. Comprehensive Email System

TooSimple sends 14+ types of transactional emails, all built with React Email for consistent branding:

// packages/email/emails/new-testimonial.tsx
export function NewTestimonialEmail({
  projectName,
  authorName,
  content,
  rating,
  dashboardUrl
}: NewTestimonialEmailProps) {
  return (
    <Html>
      <Head />
      <Body>
        <Container>
          <Heading>New testimonial for {projectName}</Heading>
          <Text>{authorName} left a {rating}-star review:</Text>
          <Section style={quoteStyle}>
            <Text>{content}</Text>
          </Section>
          <Button href={dashboardUrl}>
            View in Dashboard
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

Email types include: waitlist confirmation, welcome, email verification, password reset, plan changes, new testimonial notification, testimonial confirmation to submitter, video processing complete, weekly digest, product updates, testimonial requests, refund notification, and lifetime purchase welcome.

Key Challenges & Solutions

Challenge 1: Widget Performance on Third-Party Sites

Problem: The embed widget runs on customer websites with unknown performance characteristics. A slow or broken widget reflects poorly on both TooSimple and the customer.

Solution: Defense-in-depth performance strategy

  • Batch loading - Maximum 3 widgets initialize per 100ms to prevent browser main thread blocking
  • Lazy layout loading - Only the selected layout component is downloaded
  • Timeout protection - 10-second abort signal on all API calls
  • Retry with backoff - 3 attempts with exponential delay (1s, 2s, 4s)
  • Graceful degradation - Shows fallback UI if API is unreachable
  • IE11 compatibility - Polyfills for legacy browser support

Result: Widget loads in under 200ms on modern browsers, with graceful fallback on slow connections.

Challenge 2: Video Processing at Scale

Problem: Processing video is CPU-intensive and unpredictable. Some users upload 4K, others upload vertical phone recordings.

Solution: Dual-pipeline processing

  • Client-side: FFmpeg in browser gives instant preview (no server round-trip)
  • Server-side: Trigger.dev normalizes to 720p in the background
  • 90-second cap: Prevents abuse and keeps processing fast
  • Status tracking: Users see processing state (pending, processing, completed, failed)
  • Thumbnail generation: Auto-extracted frame at 2 seconds for previews

Result: Users see their video immediately after upload, and receive an email when the optimized version is ready.

Challenge 3: Multi-Tenant Security

Problem: Each user has their own projects, testimonials, and uploaded files. One user must never access another's data.

Solution: Multiple isolation layers

  • User-namespaced storage: R2 paths include userId ({userId}/{fileId})
  • Path validation on delete: Verifies file path belongs to requesting user before deletion
  • Project-scoped queries: All database queries filter by project ownership
  • Widget public IDs: Separate from internal IDs, rate-limited per IP
  • CORS configuration: Widget endpoints only accessible from allowed origins

Challenge 4: Pricing That Works for Bootstrappers

Problem: Competitors charge $29-99/month. I wanted to be significantly cheaper without racing to the bottom.

Solution: Simple, value-based pricing

  • Free tier: 10 testimonials with TooSimple branding (try before buying)
  • Pro: $9/month for unlimited testimonials, no branding, all features
  • Lifetime: One-time purchase option for indie hackers who hate subscriptions
  • Polar.sh as merchant of record handles taxes, refunds, and compliance

Why $9/month works:

  • Low enough that bootstrapped founders don't think twice
  • High enough to sustain development and infrastructure
  • Unlimited testimonials means no nickel-and-diming
  • Lifetime option captures the "I hate subscriptions" crowd

What I Learned

1. Monorepo structure pays for itself Sharing packages (@v1/database, @v1/email, @v1/ui) across the dashboard and marketing site eliminated duplication. Turborepo's caching means rebuilds only touch changed packages. The upfront setup cost pays back within the first week.

2. Embed widgets are their own product Building a JavaScript widget that works on any website is fundamentally different from building a web app. You can't control the host page's CSS, JavaScript, or performance. Every assumption you make about the environment is wrong. Test on real customer sites early.

3. Video is hard, but worth it Video testimonials convert 2-3x better than text. The FFmpeg + Trigger.dev pipeline was the hardest part to build, but it's the feature that differentiates TooSimple from cheaper alternatives. Don't shy away from hard technical problems if they deliver clear user value.

4. Self-hosting with Coolify reduces costs dramatically Running on a Hostinger VPS (4 vCPU, 8GB RAM) with Coolify costs $20/month total, vs $100+/month on Vercel at the same scale. The tradeoff is more ops work, but Coolify's Docker-based deployments make it manageable.

5. Social imports are a growth lever Users who import existing testimonials from Twitter, LinkedIn, or Google Reviews see immediate value. They go from "empty dashboard" to "10 testimonials ready to display" in minutes. Reducing time-to-value is the best thing you can do for activation.

6. Biome over ESLint + Prettier Switching to Biome as a unified linter/formatter was a breath of fresh air. One tool, one config, 10x faster execution. If you're starting a new project, try Biome first.

Technical Achievements

After months of development, here's what shipped:

Platform:

  • 12+ social platform imports (Twitter, LinkedIn, Reddit, YouTube, Google, and more)
  • 4 embeddable widget layouts (carousel, grid, single, wall of love)
  • Full video testimonial pipeline with FFmpeg processing
  • 14+ transactional email templates
  • Auto-approval engine with configurable rules

Performance:

  • Sub-200ms widget load time
  • Zero-dependency embed script
  • Batch loading with rate limiting
  • Triple-layer error handling (timeout, retry, fallback)

Architecture:

  • Turborepo monorepo with 10+ shared packages
  • Type-safe database queries with Drizzle ORM
  • Cloudflare R2 for zero-egress media storage
  • Trigger.dev v4 for background job processing
  • Better Auth with email + social OAuth

Developer Experience:

  • Full TypeScript coverage across all packages
  • Biome for unified linting and formatting
  • Vitest for testing with coverage
  • Playwright for end-to-end testing
  • Sentry for production error tracking

Try It Yourself

Need social proof on your website? Check out toosimple.io - create your first collection form in under 2 minutes. The free tier gives you 10 testimonials to start, and Pro is just $9/month for unlimited everything.

If you're a bootstrapped founder tired of paying $30+/month for testimonials, give it a try. I built this because I needed it myself, and I think you will too.

I'd love to hear your feedback on X or via email at [email protected].

What's Next

I'm actively working on:

  • Custom domains - Use your own domain for collection forms and widget endpoints
  • Analytics dashboard - See which testimonials drive the most engagement
  • A/B testing - Test different widget layouts to optimize conversion
  • API access - Programmatic testimonial management for developers
  • Zapier/Make integration - Connect testimonials to your existing workflows

Building in public means sharing the honest progress. TooSimple started as a frustration with overpriced tools and became a real product that solves a real problem for real founders.


Want to build your own SaaS product or need help with a monorepo architecture? I'm available for consulting and freelance projects. Get in touch.