All Docs
FeaturesCSI Teachable Replacement AppUpdated March 13, 2026

Fixing the Manage Billing 404 — v1.0.22

Fixing the Manage Billing 404 — v1.0.22

What happened

In the dashboard settings page (src/app/dashboard/settings/page.tsx, line ~42), the Manage Billing button was rendered as a plain HTML anchor:

<a href="/api/billing/portal">Manage Billing</a>

No REST route handler existed at /api/billing/portal, so every click on that button returned a 404 Not Found response.

The billing portal is actually implemented as a tRPC mutation:

// src/lib/routers/billing.ts
billing.createBillingPortal  // returns a Stripe-hosted redirect URL

Because this mutation returns a URL rather than acting as a redirect endpoint itself, the hardcoded anchor had no valid destination to point to.


How to fix it

There are two valid remediation paths. Pick the one that best fits your architecture.

Option 1 — Add a REST route handler (recommended for Server Components)

Create a new Next.js App Router route handler at:

src/app/api/billing/portal/route.ts

Inside that file, call the Stripe SDK directly and redirect the user:

import { getStripe } from '@/lib/stripe'
import { redirect } from 'next/navigation'

export async function GET(request: Request) {
  const session = await getStripe().billingPortal.sessions.create({
    customer: /* resolved customer ID from session */,
    return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard/settings`,
  })

  redirect(session.url)
}

No changes are needed in the settings page — the existing <a href="/api/billing/portal"> anchor will now resolve correctly.


Option 2 — Use a Client Component with tRPC mutation

Update the settings page (or extract a ManageBillingButton component) to call the existing tRPC mutation and handle the redirect in the browser:

'use client'

import { trpc } from '@/lib/trpc/client'

export function ManageBillingButton() {
  const { mutate, isLoading } = trpc.billing.createBillingPortal.useMutation({
    onSuccess(data) {
      window.location.href = data.url
    },
  })

  return (
    <button onClick={() => mutate()} disabled={isLoading}>
      {isLoading ? 'Redirecting…' : 'Manage Billing'}
    </button>
  )
}

Replace the hardcoded anchor in settings/page.tsx with <ManageBillingButton />.

This approach keeps all billing logic inside the tRPC layer and avoids adding a new REST endpoint.


Affected file

FileIssue
src/app/dashboard/settings/page.tsxHardcoded anchor pointing to a non-existent REST route
src/lib/routers/billing.tsContains the actual createBillingPortal tRPC mutation

Impact

  • Severity: High — all users who clicked Manage Billing received a 404 and could not access the Stripe billing portal.
  • Workaround: None available until the route handler or Client Component mutation is in place.