> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firmhouse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Customer Portal v2

> A branded, customizable account portal for subscription customers.

<div className="relative overflow-hidden bg-[#f6f8ff] pb-16 dark:bg-[#05070b]">
  <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_16%,rgba(138,195,233,0.30),transparent_32%),linear-gradient(180deg,rgba(255,255,255,0)_0%,rgba(255,255,255,0.78)_58%,rgba(255,255,255,1)_100%)] dark:bg-[radial-gradient(circle_at_50%_16%,rgba(36,84,255,0.22),transparent_32%),linear-gradient(180deg,rgba(5,7,11,0)_0%,rgba(5,7,11,0.78)_58%,rgba(5,7,11,1)_100%)]" />

  <div className="relative flex min-h-[420px] w-full items-center justify-center px-6 text-center md:min-h-[500px] md:px-8">
    <div className="relative mx-auto flex max-w-5xl flex-col items-center py-12 md:py-16">
      <p className="m-0 text-sm font-semibold uppercase tracking-normal text-[#2454FF]">Customer Portal v2</p>

      <h1 className="m-0 mt-4 max-w-4xl text-3xl font-semibold tracking-normal text-gray-950 dark:text-white md:text-5xl">
        Build amazing and unique customer membership experiences
      </h1>

      <p className="mx-auto mt-6 max-w-2xl text-base leading-7 text-gray-700 dark:text-gray-200 md:text-lg md:leading-8">
        Give subscribers a branded place to manage upcoming orders, invoices, products, delivery timing, and subscription details without sending them through a generic self-service flow.
      </p>

      <div className="mt-9 flex flex-col items-center justify-center gap-3 sm:flex-row">
        <a href="/customer-portal-v2/quickstart" className="inline-flex min-h-12 items-center justify-center rounded-full bg-gray-950 px-6 text-base font-medium text-white shadow-sm transition hover:bg-gray-800 dark:bg-white dark:text-gray-950 dark:hover:bg-gray-100">
          Start using Customer Portal
        </a>

        <a href="/customer-portal-v2/components/overview" className="inline-flex min-h-12 items-center justify-center rounded-full bg-white/45 px-6 text-base font-medium text-gray-950 ring-1 ring-black/10 backdrop-blur transition hover:bg-white/65 dark:bg-white/10 dark:text-white dark:ring-white/15 dark:hover:bg-white/15">
          Build templates and use components
        </a>
      </div>
    </div>
  </div>

  <div data-customer-portal-gallery className="relative z-10 mx-auto -mt-6 block max-w-6xl px-4 sm:px-6 md:-mt-12">
    <div className="overflow-hidden rounded-2xl bg-cover bg-center shadow-[0_28px_90px_rgba(15,23,42,0.18)] ring-1 ring-black/10" style={{ backgroundImage: "url('https://mintcdn.com/firmhousebv/0shVWCZYbGwcvSV4/images/customer-portal-v2-gallery-backdrop.png?fit=max&auto=format&n=0shVWCZYbGwcvSV4&q=85&s=6974a7c5dbede5266061c945c0acfdfc')" }}>
      <div data-customer-portal-rail onScroll={(event) => { const rail = event.currentTarget; const gallery = rail.closest("[data-customer-portal-gallery]"); const activeIndex = Math.round(rail.scrollLeft / rail.clientWidth); gallery?.querySelectorAll("[data-customer-portal-thumbnail]").forEach((thumbnail, thumbnailIndex) => { thumbnail.dataset.active = thumbnailIndex === activeIndex ? "true" : "false"; }); }} className="flex snap-x snap-mandatory overflow-x-auto scroll-smooth">
        <div className="min-w-full snap-center">
          <div className="relative">
            <div role="img" aria-label="Oot Customer Portal subscription dashboard" className="aspect-video w-full bg-contain bg-center bg-no-repeat" style={{ backgroundImage: "url('https://mintcdn.com/firmhousebv/0shVWCZYbGwcvSV4/images/customer-portal-v2-oot-dashboard-window.png?fit=max&auto=format&n=0shVWCZYbGwcvSV4&q=85&s=ab8e93e74846ee90840bd6460e6e99f3')" }} />

            <button type="button" aria-label="Previous portal sample" onClick={(event) => { const rail = event.currentTarget.closest("[data-customer-portal-gallery]")?.querySelector("[data-customer-portal-rail]"); rail?.scrollTo({ left: rail.clientWidth * 2, behavior: "smooth" }); }} className="absolute left-4 top-1/2 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-gray-950 shadow-sm ring-1 ring-black/10 transition hover:bg-white dark:bg-gray-950/80 dark:text-white dark:ring-white/15 dark:hover:bg-gray-950">
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5" aria-hidden="true">
                <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
              </svg>
            </button>

            <button type="button" aria-label="Next portal sample" onClick={(event) => { const rail = event.currentTarget.closest("[data-customer-portal-gallery]")?.querySelector("[data-customer-portal-rail]"); rail?.scrollTo({ left: rail.clientWidth, behavior: "smooth" }); }} className="absolute right-4 top-1/2 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-gray-950 shadow-sm ring-1 ring-black/10 transition hover:bg-white dark:bg-gray-950/80 dark:text-white dark:ring-white/15 dark:hover:bg-gray-950">
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5" aria-hidden="true">
                <path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
              </svg>
            </button>
          </div>
        </div>

        <div className="min-w-full snap-center">
          <div className="relative">
            <div role="img" aria-label="Oot Customer Portal product detail overlay" className="aspect-video w-full bg-contain bg-center bg-no-repeat" style={{ backgroundImage: "url('https://mintcdn.com/firmhousebv/0shVWCZYbGwcvSV4/images/customer-portal-v2-oot-product-overlay-window.png?fit=max&auto=format&n=0shVWCZYbGwcvSV4&q=85&s=09b2448012968dee8c161160db00f495')" }} />

            <button type="button" aria-label="Previous portal sample" onClick={(event) => { const rail = event.currentTarget.closest("[data-customer-portal-gallery]")?.querySelector("[data-customer-portal-rail]"); rail?.scrollTo({ left: 0, behavior: "smooth" }); }} className="absolute left-4 top-1/2 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-gray-950 shadow-sm ring-1 ring-black/10 transition hover:bg-white dark:bg-gray-950/80 dark:text-white dark:ring-white/15 dark:hover:bg-gray-950">
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5" aria-hidden="true">
                <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
              </svg>
            </button>

            <button type="button" aria-label="Next portal sample" onClick={(event) => { const rail = event.currentTarget.closest("[data-customer-portal-gallery]")?.querySelector("[data-customer-portal-rail]"); rail?.scrollTo({ left: rail.clientWidth * 2, behavior: "smooth" }); }} className="absolute right-4 top-1/2 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-gray-950 shadow-sm ring-1 ring-black/10 transition hover:bg-white dark:bg-gray-950/80 dark:text-white dark:ring-white/15 dark:hover:bg-gray-950">
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5" aria-hidden="true">
                <path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
              </svg>
            </button>
          </div>
        </div>

        <div className="min-w-full snap-center">
          <div className="relative">
            <div role="img" aria-label="Boombrush Customer Portal subscription dashboard" className="aspect-video w-full bg-contain bg-center bg-no-repeat" style={{ backgroundImage: "url('https://mintcdn.com/firmhousebv/0shVWCZYbGwcvSV4/images/customer-portal-v2-boombrush-dashboard-window.png?fit=max&auto=format&n=0shVWCZYbGwcvSV4&q=85&s=5f5a7a364c7f95fd61172bf51f5774fc')" }} />

            <button type="button" aria-label="Previous portal sample" onClick={(event) => { const rail = event.currentTarget.closest("[data-customer-portal-gallery]")?.querySelector("[data-customer-portal-rail]"); rail?.scrollTo({ left: rail.clientWidth, behavior: "smooth" }); }} className="absolute left-4 top-1/2 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-gray-950 shadow-sm ring-1 ring-black/10 transition hover:bg-white dark:bg-gray-950/80 dark:text-white dark:ring-white/15 dark:hover:bg-gray-950">
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5" aria-hidden="true">
                <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
              </svg>
            </button>

            <button type="button" aria-label="Next portal sample" onClick={(event) => { const rail = event.currentTarget.closest("[data-customer-portal-gallery]")?.querySelector("[data-customer-portal-rail]"); rail?.scrollTo({ left: 0, behavior: "smooth" }); }} className="absolute right-4 top-1/2 flex h-10 w-10 -translate-y-1/2 items-center justify-center rounded-full bg-white/90 text-gray-950 shadow-sm ring-1 ring-black/10 transition hover:bg-white dark:bg-gray-950/80 dark:text-white dark:ring-white/15 dark:hover:bg-gray-950">
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5" aria-hidden="true">
                <path strokeLinecap="round" strokeLinejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
              </svg>
            </button>
          </div>
        </div>
      </div>
    </div>

    <div className="mt-4 grid grid-cols-3 gap-3">
      <button type="button" aria-label="Show Oot dashboard portal sample" data-customer-portal-thumbnail data-active="true" onClick={(event) => { const gallery = event.currentTarget.closest("[data-customer-portal-gallery]"); const rail = gallery?.querySelector("[data-customer-portal-rail]"); gallery?.querySelectorAll("[data-customer-portal-thumbnail]").forEach((thumbnail, thumbnailIndex) => { thumbnail.dataset.active = thumbnailIndex === 0 ? "true" : "false"; }); rail?.scrollTo({ left: 0, behavior: "smooth" }); }} className="group overflow-hidden rounded-lg bg-white text-left opacity-75 ring-1 ring-black/10 transition hover:opacity-100 hover:ring-[#2454FF] data-[active=true]:opacity-100 data-[active=true]:ring-2 data-[active=true]:ring-[#2454FF] dark:bg-white/10 dark:ring-white/10">
        <span aria-hidden="true" className="block aspect-video w-full bg-cover bg-center opacity-80 transition group-hover:opacity-100" style={{ backgroundImage: "url('https://mintcdn.com/firmhousebv/0shVWCZYbGwcvSV4/images/customer-portal-v2-oot-dashboard-retina.png?fit=max&auto=format&n=0shVWCZYbGwcvSV4&q=85&s=40afaa9b1f0b7cb1cc47154b66fdddc4')" }} />
      </button>

      <button type="button" aria-label="Show Oot product overlay portal sample" data-customer-portal-thumbnail data-active="false" onClick={(event) => { const gallery = event.currentTarget.closest("[data-customer-portal-gallery]"); const rail = gallery?.querySelector("[data-customer-portal-rail]"); gallery?.querySelectorAll("[data-customer-portal-thumbnail]").forEach((thumbnail, thumbnailIndex) => { thumbnail.dataset.active = thumbnailIndex === 1 ? "true" : "false"; }); rail?.scrollTo({ left: rail.clientWidth, behavior: "smooth" }); }} className="group overflow-hidden rounded-lg bg-white text-left opacity-75 ring-1 ring-black/10 transition hover:opacity-100 hover:ring-[#2454FF] data-[active=true]:opacity-100 data-[active=true]:ring-2 data-[active=true]:ring-[#2454FF] dark:bg-white/10 dark:ring-white/10">
        <span aria-hidden="true" className="block aspect-video w-full bg-cover bg-center opacity-80 transition group-hover:opacity-100" style={{ backgroundImage: "url('https://mintcdn.com/firmhousebv/0shVWCZYbGwcvSV4/images/customer-portal-v2-oot-product-overlay-retina.png?fit=max&auto=format&n=0shVWCZYbGwcvSV4&q=85&s=ffbe72daf3f6d68038c9120dde85de02')" }} />
      </button>

      <button type="button" aria-label="Show Boombrush dashboard portal sample" data-customer-portal-thumbnail data-active="false" onClick={(event) => { const gallery = event.currentTarget.closest("[data-customer-portal-gallery]"); const rail = gallery?.querySelector("[data-customer-portal-rail]"); gallery?.querySelectorAll("[data-customer-portal-thumbnail]").forEach((thumbnail, thumbnailIndex) => { thumbnail.dataset.active = thumbnailIndex === 2 ? "true" : "false"; }); rail?.scrollTo({ left: rail.clientWidth * 2, behavior: "smooth" }); }} className="group overflow-hidden rounded-lg bg-white text-left opacity-75 ring-1 ring-black/10 transition hover:opacity-100 hover:ring-[#2454FF] data-[active=true]:opacity-100 data-[active=true]:ring-2 data-[active=true]:ring-[#2454FF] dark:bg-white/10 dark:ring-white/10">
        <span aria-hidden="true" className="block aspect-video w-full bg-cover bg-center opacity-80 transition group-hover:opacity-100" style={{ backgroundImage: "url('https://mintcdn.com/firmhousebv/i0tAuZcoMZU4WKIb/images/customer-portal-v2-boombrush-dashboard-retina.png?fit=max&auto=format&n=i0tAuZcoMZU4WKIb&q=85&s=23fb7cef219f38b182d698b0f4562ead')" }} />
      </button>
    </div>
  </div>

  <div className="hidden">
    <div className="relative overflow-hidden rounded-2xl border border-gray-200 bg-white/80 p-6 shadow-sm dark:border-white/10 dark:bg-white/[0.04] md:p-8">
      <div className="pointer-events-none absolute inset-0 dark:hidden" style={{ backgroundImage: "linear-gradient(rgba(36,84,255,0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(36,84,255,0.06) 1px, transparent 1px)", backgroundSize: "28px 28px" }} />

      <div className="pointer-events-none absolute inset-0 hidden dark:block" style={{ backgroundImage: "linear-gradient(rgba(255,255,255,0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.06) 1px, transparent 1px)", backgroundSize: "28px 28px" }} />

      <div className="relative">
        <h2 className="m-0 text-xl font-semibold tracking-normal text-gray-950 dark:text-white md:text-2xl">
          Benefits and features
        </h2>

        <div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
          <div className="rounded-lg border border-gray-200 bg-white/85 p-4 dark:border-white/10 dark:bg-[#05070b]/70">
            <h3 className="m-0 text-base font-semibold text-gray-950 dark:text-white">Branded membership experiences</h3>

            <p className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-300">
              Placeholder copy for explaining how merchants can make the portal feel like part of their own storefront and membership program.
            </p>
          </div>

          <div className="rounded-lg border border-gray-200 bg-white/85 p-4 dark:border-white/10 dark:bg-[#05070b]/70">
            <h3 className="m-0 text-base font-semibold text-gray-950 dark:text-white">Customer self-service</h3>

            <p className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-300">
              Placeholder copy for subscription, order, invoice, payment, and account actions customers can handle themselves.
            </p>
          </div>

          <div className="rounded-lg border border-gray-200 bg-white/85 p-4 dark:border-white/10 dark:bg-[#05070b]/70">
            <h3 className="m-0 text-base font-semibold text-gray-950 dark:text-white">Flexible templates</h3>

            <p className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-300">
              Placeholder copy for Liquid templates, reusable components, and custom portal layouts.
            </p>
          </div>

          <div className="rounded-lg border border-gray-200 bg-white/85 p-4 dark:border-white/10 dark:bg-[#05070b]/70">
            <h3 className="m-0 text-base font-semibold text-gray-950 dark:text-white">Preview before rollout</h3>

            <p className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-300">
              Placeholder copy for testing portal changes, previewing template packages, and publishing when ready.
            </p>
          </div>

          <div className="rounded-lg border border-gray-200 bg-white/85 p-4 dark:border-white/10 dark:bg-[#05070b]/70">
            <h3 className="m-0 text-base font-semibold text-gray-950 dark:text-white">Shopify data and account entry</h3>

            <p className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-300">
              Placeholder copy for bringing Shopify storefront data, customer metafields, and account portal entry points into the experience.
            </p>
          </div>

          <div className="rounded-lg border border-gray-200 bg-white/85 p-4 dark:border-white/10 dark:bg-[#05070b]/70">
            <h3 className="m-0 text-base font-semibold text-gray-950 dark:text-white">Built for subscriptions</h3>

            <p className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-300">
              Placeholder copy for recurring orders, delivery timing, product swaps, add-ons, and subscription lifecycle controls.
            </p>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

## Migrating legacy Self Service Center content

When Customer Portal v2 is rolled out for a project, legacy **Settings > Self Service Center** content blocks no longer control the new customer-facing pages. Custom content that was previously configured in the legacy Self Service Center needs to be recreated in the Customer Portal template that renders that page.

For example, if you previously used a legacy Self Service Center promotion block on the dashboard, move that logic into **Customer Portal > Templates > `dashboard.liquid`**. Save a new template version, preview it with a real subscription, and publish the version when you are ready for customers to see the change.
