Stop Paying Shopify for your Single Product Store

Davis GDavis G
4 min read

E-commerce solutions can be prohibitively expensive for low volume sellers. When launching my single product store baybikekey.com I struggled to find any platform that satisfied my requirements: custom domain, custom theme, low monthly fee.

The Roundup

ServiceMonthly PriceCustom DomainCustom Theme
Shopify Starter$5 + higher tx fees
Shopify Basic$29
Wix Free$0
Wix Business Basic$27
Ecwid Free$0
Ecwid Venture$21

As you can see there is some borderline sketchy price fixing going on, to sell a product on a custom domain will have you shelling out hundreds a year. Shopify Starter was the most appealing option I found, but I soon learned about several illogical restrictions like the inability to add a cartless “Buy Now” button. I found this to be a dealbreaker.

For small time sellers who only plan to sell on the order of thousands in revenue per store, this cost isn’t feasible. I was determined to find something better.

I started browsing the wide library of Vercel templates and came across the Shirt Shop which seemed ideal for my single product landing page use case.

Vercel has had an interesting journey into e-commerce: v1 of their commerce platform stayed agnostic and offered a choice of providers while v2 went all in on Shopify. Fortunately this template lacks any commerce implementation which left me free to implement my own solution.

Enter Stripe

I decided that I would explore dropping one level lower from an “e-commerce platform” to a “payment processor” and use Stripe for checkout. Despite being a payment processor they offer powerful checkout customization and only charge the standard 2.9% per transaction. There are some drawbacks with fulfillment and inventory management but some emerging apps have made this situation a bit better.

After populating my product details in Stripe I had my checkout button wired up with just a few lines. The product page initializes a Stripe client and fetches the default product which populate the product heading and image gallery. The checkout button makes a post request to a new route /api/payment.

// product/page.tsx

const product = await stripe.products.retrieve(
  process.env.STRIPE_PRODUCT_ID!,
  {
    expand: ["default_price"],
  }
);

...

return (<>
<ProductHeading stripeProduct={product} />

<ImageGallery stripeProduct={product} />

<div className="mt-8 lg:col-span-5">
  <form action="/api/payment" method="POST">
    <input type="hidden" id="promo" name="promo" value={promoCode ? promoCode.code : undefined}></input>
    <button
      type="submit"
      className="mt-8 flex w-full items-center justify-center rounded-md border border-transparent bg-sky-400 px-8 py-3 text-base font-medium text-white hover:bg-sky-300 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2">
      Checkout
    </button>
  </form>
</div>
</>);

And then I defined the api/payment route

// api/payment/route.tsx

import Stripe from "stripe";
import { NextResponse, NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

  const session = await stripe.checkout.sessions.create({
    line_items: [
      {
        price: process.env.STRIPE_PRODUCT_PRICE!,
        quantity: 1,
        adjustable_quantity: {
          enabled: true,
          minimum: 1,
          maximum: 499,
        },
      },
    ],
    allow_promotion_codes: true,
    mode: "payment",
    success_url: request.headers.get('referer') + "?success=true",
    cancel_url: request.headers.get('referer'),
    shipping_address_collection: {
      allowed_countries: ["US"],
    },
    shipping_options: [
      {
        shipping_rate: process.env.STRIPE_SHIPPING_RATE!,
      },
    ],
    phone_number_collection: {
      enabled: true,
    },
  });

  return NextResponse.redirect(session.url!, { status: 303 });
}

With that I had a functioning single product store.

Stripe Drawbacks

  1. Inventory

One immediate thing I missed coming from Shopify was inventory tracking and control. There is no notion of remaining inventory on a Stripe product. This is something I’ll have to monitor and halt sales manually when I run out of stock.

  1. Fullfillment

The other major drawback of Stripe is the lack of fulfillment tools. For a moment I thought I was going to need to manually copy shipping addresses over to a tool like EasyPost and track fulfilled orders in a spreadsheet, until I discovered Parcelcraft.

ParcelCraft

Stripe has a rich app ecosystem and I discovered a new app called Parcelcraft. This app bridges the fulfillment gap with Stripe and allows 1 click shipping from the Stripe dashboard. It tracks unfulfilled orders, emails tracking information to customers, and has powerful printing integrations like Printnode to print directly to label printers.

With that piece of the puzzle solved I was unblocked to launch my single product store and I don’t see myself returning to a paid e-commerce platform again.

0
Subscribe to my newsletter

Read articles from Davis G directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Davis G
Davis G