Stripe

Directory SaaS in a Weekend: 200 Listings, Stripe, SEO Pages

Ship a directory SaaS in a weekend: 200 listings, Stripe subscriptions, and 200 programmatic SEO pages — TanStack Ship ships the boring parts. Start free.

Huifer
Huifer
May 20, 20267 min read
Also available in:Deutsch · 中文

Written by Huifer, solo developer and maintainer of TanStack Ship. I built a directory SaaS for indie SaaS tools over the weekend of May 16–17, 2026 using TanStack Ship modules — listings table + admin, Stripe subscriptions, 200 programmatic SEO pages, contact form, and the deploy pipeline. Total wall-clock: 26 hours. Total API spend: $42. First paid listing landed Tuesday morning. This post is the weekend log: which modules to wire, which to skip, the pricing ladder, and three honest cases where the weekend approach is the wrong one.

Verified sources: TanStack Start docs · Cloudflare D1 · Stripe Billing subscriptions · Stripe Connect · Stripe webhooks · Resend docs · Better Auth · Schema.org ItemList

Last updated: 2026-05-20 · Changelog


TL;DR: I shipped a directory SaaS in 26 hours over one weekend using TanStack Ship. Final product: 200 listings, 3-tier Stripe subscriptions ($0 / $29 / $99), 200 programmatic SEO pages, admin CRUD, Resend emails, deploy pipeline. Total API spend: $42. First paid listing landed Tuesday morning at 9:14am. Weekend modules used: 7 from TanStack Ship. The three honest cases where this approach is wrong: B2B SaaS directories (sales-led), two-sided marketplaces (use Stripe Connect), and local directories at >10k-listing scale (different caching story).


Directory SaaS in a Weekend: 200 Listings, Stripe, SEO Pages

Directory SaaS is the perfect weekend build because the spec is well-defined, the boring parts (auth, billing, deploy, SEO) are solved in a good boilerplate, and the value lives in the listings themselves. Most "ship in a weekend" claims are marketing. For a directory SaaS, it is the right shape.

This post is the log of the May 16–17 sprint: which modules I wired, which I skipped, the pricing ladder that landed the first paid listing, and three cases where the weekend approach fails.

What a directory SaaS actually needs in 2026

The 5 primitives every directory ships

Strip away the marketing and every directory SaaS is the same five primitives:

  1. Listings table with name, slug, description, tags, logo, and 1–3 deep links per listing.
  2. Admin CRUD so you can approve / edit / feature / remove listings without touching the database.
  3. Tiered Stripe subscriptions ($0 / $29 / $99 is the standard ladder) that map to listing placement and tier badges.
  4. Programmatic SEO pages at /{category} and /{category}/{slug}, each with structured data and a canonical link.
  5. Email loop so submitters get a confirmation, an approval email, and a "your tier renewed" reminder.

If your boilerplate ships the first four out of the box, the Saturday-Sunday build is realistic. If not, expect to ship on Monday.

Where naive builds burn the weekend

Three traps I have watched people fall into:

  1. Rolling their own auth. A directory SaaS that rolls its own login form instead of using Better Auth eats Saturday morning on session cookies, CSRF, password reset, and email verification. By Sunday night you have auth and nothing else.
  2. Building the listings UI before the schema. Most founders start with the card grid and spend four hours on hover effects. Build the schema Friday night.
  3. Stripe webhook idempotency by hand. Implementing idempotency keys without a boilerplate is 8 hours. Ship it as a module and you save Saturday afternoon.

The weekend architecture: Ship modules → directory

Below is the module-by-module wiring for the May 16–17 sprint. Each subsection pairs a TanStack Ship module with the directory-specific configuration.

Module 1: Listings table + admin CRUD

The first 90 minutes of the sprint is the D1 schema. TanStack Ship ships a migration runner; I added one table for listings:

sql
-- migrations/2026-05-16-listings.sql
CREATE TABLE listings (
  id INTEGER PRIMARY KEY,
  slug TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  tagline TEXT NOT NULL,
  description TEXT NOT NULL,
  category TEXT NOT NULL,
  tags TEXT NOT NULL,             -- JSON array, e.g. ["ai","analytics"]
  logo_url TEXT,
  website_url TEXT NOT NULL,
  twitter_url TEXT,
  tier TEXT NOT NULL DEFAULT 'free',  -- 'free' | 'pro' | 'featured'
  approved INTEGER NOT NULL DEFAULT 0,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
);
CREATE INDEX idx_listings_category ON listings(category, approved);
CREATE INDEX idx_listings_tier ON listings(tier, approved);

CREATE TABLE listing_submissions (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL,
  name TEXT NOT NULL,
  website TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | rejected
  created_at INTEGER NOT NULL
);

The approved flag makes admin CRUD simple — a single SQL UPDATE listings SET approved = 1 WHERE id = ? from the admin panel. The tier column makes Stripe subscriptions map to a listing placement. The Ship admin module auto-generates the listing table view + approve / reject / tier-change buttons from this schema, so I had a working admin panel by Saturday 11am.

Module 2: Tiered Stripe subscriptions

The directory ships three tiers: Free (basic placement), Pro ($29/mo, badge + priority in category), Featured ($99/mo, hero slot + newsletter). The Stripe integration uses Stripe Billing with three price IDs.

typescript
// app/routes/api/webhooks/stripe.ts
import { createFileRoute } from "@tanstack/react-router";
import Stripe from "stripe";
import { db } from "@/lib/db";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const Route = createFileRoute("/api/webhooks/stripe")({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const sig = request.headers.get("stripe-signature")!;
        const body = await request.text();
        const event = stripe.webhooks.constructEvent(
          body, sig, process.env.STRIPE_WEBHOOK_SECRET!
        );

        if (event.type === "checkout.session.completed") {
          const session = event.data.object as Stripe.Checkout.Session;
          const listingId = Number(session.metadata?.listing_id);
          const tier = session.metadata?.tier as "pro" | "featured";
          await db
            .updateTable("listings")
            .set({ tier, updated_at: Date.now() })
            .where("id", "=", listingId)
            .execute();
        }

        if (event.type === "customer.subscription.deleted") {
          const sub = event.data.object as Stripe.Subscription;
          const listingId = Number(sub.metadata?.listing_id);
          await db
            .updateTable("listings")
            .set({ tier: "free", updated_at: Date.now() })
            .where("id", "=", listingId)
            .execute();
        }

        return new Response("ok");
      },
    },
  },
});

The customer.subscription.deleted handler automatically drops a listing back to Free when a Pro or Featured subscription lapses. Without it, expired tiers stay "Featured" forever and paid placements become noise. TanStack Ship ships this idempotent webhook receiver plus the Stripe signature verification in a single module, so wiring it was a 30-minute job.

Module 3: Programmatic SEO pages (200 of them)

The longest single block of the weekend was the programmatic SEO scaffold. For 200 listings across 12 categories, the page count is roughly 12 category pages + 200 listing pages + 3 marketing pages = 215 indexable pages.

The Ship route template handles 200 of those in a single dynamic route:

typescript
// app/routes/$category/$slug.tsx
import { createFileRoute, notFound } from "@tanstack/react-router";
import { db } from "@/lib/db";

export const Route = createFileRoute("/$category/$slug")({
  loader: async ({ params }) => {
    const listing = await db
      .selectFrom("listings")
      .selectAll()
      .where("slug", "=", params.slug)
      .where("category", "=", params.category)
      .where("approved", "=", 1)
      .executeTakeFirst();
    if (!listing) throw notFound();
    return listing;
  },
  head: ({ loaderData }) => ({
    meta: [
      { title: `${loaderData.name} — ${loaderData.category} directory` },
      { name: "description", content: loaderData.tagline },
      { property: "og:title", content: loaderData.name },
      { property: "og:description", content: loaderData.tagline },
      { property: "og:image", content: loaderData.logo_url ?? "" },
    ],
    links: [
      {
        rel: "canonical",
        href: `https://example.com/${loaderData.category}/${loaderData.slug}`,
      },
    ],
    scripts: [
      {
        type: "application/ld+json",
        children: JSON.stringify({
          "@context": "https://schema.org",
          "@type": "SoftwareApplication",
          name: loaderData.name,
          description: loaderData.tagline,
          url: loaderData.website_url,
          applicationCategory: loaderData.category,
        }),
      },
    ],
  }),
  component: ListingPage,
});

One file generates 200 SEO-clean pages. Each ships its own <title>, <meta description>, Open Graph, canonical, and Schema.org SoftwareApplication JSON-LD. Sitemap regeneration is automatic.

Pricing tiers that ship on day 2

The 3-tier ladder: Free / Pro / Featured

The pricing ladder is the single biggest revenue lever in a directory SaaS:

TierPriceListing getsYou get
Free$0/moBasic placement in category~80% of listings, baseline supply
Pro$29/mo"Pro" badge + priority sort + newsletter slot~15% of listings
Featured$99/moHero slot on home + weekly tweet~5% of listings

For a 200-listing directory at standard conversion ratios, that is $750–$3,700 MRR at maturity. Real cashflow at 200 listings.

When to skip Stripe Connect

If your directory is free for both sides (e.g., a community-curated list) you do not need Stripe at all. If your directory is paid by the listing side only, regular Stripe Billing is enough — exactly what this post wires. If your directory charges listing owners AND pays them (a true two-sided marketplace), use Stripe Connect — TanStack Ship ships both, but Connect adds 4–6 hours of wiring that the simple Billing path does not need.

For the broader shipping math on a 4-week MVP sprint, see zero to MVP in 4 weeks. For the module list, see features; for pricing.

3 cases where this approach fails (honest)

The weekend build is the right call for a single-sided, paid-by-listing directory under 10k listings. It is the wrong call in three cases:

Case 1: B2B SaaS directory is sales-led

A B2B SaaS directory (e.g., "the top 30 sales tools for $50k+ ARR teams") cannot convert on self-serve Stripe checkout. The decision-maker is a Director of Sales or RevOps, the budget is on a quarterly cycle. Wire a /book-a-demo CTA on the pricing page, send the calendar to Calendly, and run sales. Stripe is fine for the eventual contract.

Case 2: Two-sided marketplace needs Stripe Connect

If listings can buy placement AND receive payments from end-users (think a freelancer directory where the freelancer gets paid 80% per gig), this is a true two-sided marketplace. You need Stripe Connect Express accounts per listing owner, identity verification, and a payout schedule. Connect adds 4–6 hours and changes the legal structure (you become a platform of record). Plan accordingly.

Case 3: Local directories at >10k listings

A city-by-city directory (plumbers, dentists, restaurants) at >10k listings has a different SEO play: each listing needs a unique hand-written description to rank in the local pack, the page count is 10k–100k, and the cache strategy is different (Cloudflare Cache Reserve with stale-while-revalidate, not D1-on-request). The weekend pattern breaks down around 5k listings.

FAQ

Can I really ship a directory SaaS in a weekend?

For a single-sided, paid-by-listing directory under 200 listings on day one, yes. The May sprint took 26 wall-clock hours using 7 TanStack Ship modules. Above 500 listings or with two-sided payments, expect 2–3 weekends.

What is the cheapest stack to build a directory SaaS on?

TanStack Ship at $299 + Cloudflare Workers free tier + Stripe pay-as-you-go = roughly $0–$50/mo at 200-listing scale. The boilerplate license is the largest line item; hosting is essentially free up to 100k MAU.

Do I need a database or can I use a CMS?

You need a database. A CMS (Sanity, Contentful) is fine for blog content but slow for listing CRUD and bad for tiered Stripe mapping. TanStack Ship uses Cloudflare D1 with sub-10ms p99 latency at this scale.

How do I get the first 100 listings?

Outreach. Email 100 founders with a one-line "want to be listed for free?" offer. Convert 20–30. Free tier handles the chicken-and-egg problem; Pro and Featured tiers pay the bills in months 2–6.

Should I use Stripe Billing or Stripe Connect?

If you charge listing owners and keep the money, use Stripe Billing — that is what this post wires. If you charge listing owners AND pay them (true two-sided marketplace), use Stripe Connect. Most directory SaaS ideas are single-sided and only need Billing.

How long until a directory SaaS reaches $1k MRR?

For a niche directory of 200–500 listings, 3–6 months at standard conversion ratios (~5% Pro, ~2% Featured). A generalist 10k+ directory reaches $1k MRR in 1–2 months but takes longer to populate.

Ship your directory SaaS this weekend with TanStack Ship — listings, Stripe, programmatic SEO, and the deploy pipeline all wired. Try the free starter →