# {{capProjectName}} {{capAppName}} — billing & account portal

The lean "manage your account" surface customers visit outside the product:
plan, payment method, invoices, API keys, profile. Consumes the
**api-saas-starter** backend; billing is plugin-orchestrated, so the template
never renders an invoice or holds a card. Bilingual (en/de).

```bash
voltro init my-saas --api=api-saas-starter --web=frontend-portal
```

## Pages

| URL | Reads / drives | Notes |
|---|---|---|
| `/` | `billing.subscription` | overview: plan, status, renewal + quick links |
| `/billing` | `billing.subscription`, `billing.portalUrl`, `billing.previewChange`, `billing.changePlan` | plan card; card management redirects to the provider portal; a plan change QUOTES the provider's proration before applying |
| `/invoices` | `billing.invoices` | list; "View" / "PDF" link out to the provider's hosted pages |
| `/api-keys` | `GET/POST /v1/api-keys*` | the framework's built-in key management (see below) |
| `/profile` | `session.me`, `/auth/password-reset`, `/auth/sign-out` | identity, self-serve password reset, sign out |
| `/login` | `POST /auth/*` | sign-in / sign-up |

`(portal)/layout.tsx` is the SSR auth gate — it asks `session.me` server-side and
RedirectErrors an anonymous visitor to `/login`.

## API keys — one config step on the API

The API-keys page drives the framework's **built-in** management surface
(`/v1/api-keys`), which the API exposes only when it sets `apiKeys: true` in its
`app.config.ts`. **api-saas-starter does not enable it by default**, so out of the
box this page shows a clear "enable it" notice rather than a broken table. To make
it live:

1. On the API: `export default { …, apiKeys: true }`.
2. Grant the signed-in user an issuing scope (`apikey:issue:self`) or `admin` —
   the management routes are admin-scoped by design (keys are org credentials).

The raw key is shown exactly **once** at mint time (hash-stored, unrecoverable).
The page degrades on `404` (not enabled), `401/403` (no scope), and errors.

## What's intentionally NOT here

Per the lean brief: **no** support-ticket system and **no** team/member
management — those belong in the product's own dashboard or a dedicated tool.
Seat changes stay with the dashboard (`billing.changeSeats`); this portal is plan,
payment, invoices, keys, profile.

## Files

```
app.config.ts                       web app; apis.app → @{{projectName}}/api (api-saas-starter)
src/lib/api.ts                      wire shapes (subscription/invoice/apiKey) + formatMoney
src/pages/
  login/page.tsx(.test)             sign-in / sign-up (POST /auth/*)
  (portal)/layout.tsx(.test)        shell + SSR auth gate (session.me)
  (portal)/page.tsx                 overview
  (portal)/billing/page.tsx(.test)  plan + payment method + quoted plan change
  (portal)/invoices/page.tsx        invoice history (links out)
  (portal)/api-keys/page.tsx(.test) framework key management (graceful)
  (portal)/profile/page.tsx         identity + password reset + sign out
  (portal)/error.tsx · not-found.tsx
src/locales/{en,de}.ts              bilingual catalogs (parity-enforced)
```

## Going to production

- **Real Stripe** — configure `billingPlugin({ provider: 'stripe', … })` on the
  API; `billing.portalUrl` then opens Stripe's real hosted portal and
  `billing.invoices` returns real `hostedUrl`/`pdfUrl` links.
- **Email** — wire `sendEmail` on the API so the profile password-reset actually
  delivers.
