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

# Self-service portal

> A hosted surface where your end users sign in with their license key, verify by email, and manage their own HWID resets.

The self-service portal lets your customers handle routine license tasks themselves; without opening a support ticket. Buyers go to `portal.authforge.cc`, enter their license key and email, receive a magic code, and land on a page where they can see license status and reset their HWID subject to policy you define.

## Why it exists

HWID resets are the single most common support request for license-gated software. The portal gives customers a policy-controlled way to do it themselves:

* **Fewer tickets**: users reset their own devices when their policy allows it.
* **Auditable**: every reset is logged on the license with timestamp and source.
* **Safe by default**: limits and cooldowns are enforced server-side, not suggested.
* **Branded**: accent color and display name are published by your app.

## How sign-in works

```mermaid theme={null}
sequenceDiagram
    participant User
    participant Portal as portal.authforge.cc
    participant API as portal-api.authforge.cc
    participant SES as Amazon SES
    participant DB as Licenses

    User->>Portal: Enter license key + email
    Portal->>API: POST /portal/challenges
    API->>DB: Look up license
    API->>API: Compare email (constant-time)
    API->>SES: Send 6-digit code
    API-->>Portal: Generic "code sent" response
    User->>Portal: Enter 6-digit code
    Portal->>API: POST /portal/sessions
    API-->>Portal: HMAC-signed session token
    Portal->>API: GET /portal/me (license + policy)
```

A few deliberate properties of this flow:

* **Constant-time response.** The portal always returns the same "if the license and email match, a code was sent" message, whether or not the license exists. This prevents the portal from being used to probe for valid keys.
* **Short-lived challenges.** Challenges are stored in DynamoDB with a TTL and are single-use.
* **Stateless sessions.** Session tokens are HMAC-signed with a secret in AWS Secrets Manager. No session table to scale.

## Email requirement

The portal authenticates on **license key + email**. A license without an email attached is invisible to the portal, by design.

* **Commerce-created licenses**: the buyer’s email is attached automatically by the [Commerce pipeline](/features/commerce). No action required.
* **Manually-created licenses**: you must attach an email before the portal will recognize it.

### Attaching an email (dashboard)

1. Open the app’s **Licenses** tab.
2. Click the row menu on the license → **Portal Email**.
3. Enter the customer’s email (or clear it). Save.

Licenses with an email attached show a small mail icon next to the key, so you can spot portal-ready rows at a glance.

### Attaching an email (Developer API)

```bash theme={null}
curl -X PUT https://api.authforge.cc/v1/licenses/LICENSE_KEY/email \
  -H "Authorization: Bearer af_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "email": "customer@example.com" }'
```

Pass `"email": null` or `""` to clear it. Requires the `write:licenses` scope.

You can also pass an `email` field when creating licenses via `POST /v1/licenses`; see the [Licenses API](/api/licenses).

## Policy

Each app has a **portal policy** controlling what end users can do. Configure it in the dashboard under **Settings → Portal**.

| Field                          | Purpose                                                                                                                                                                                                                                                 |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HWID reset limit               | Maximum resets allowed inside the window.                                                                                                                                                                                                               |
| Reset window                   | Sliding window (for example 30 days) over which the limit applies.                                                                                                                                                                                      |
| Cooldown                       | Minimum time between consecutive resets.                                                                                                                                                                                                                |
| Support URL                    | Optional link shown to users who hit their limit. Auto-prefixed with `https://` if the scheme is omitted.                                                                                                                                               |
| Support email                  | Optional `mailto:` link. Also used as the reply-to hint in Commerce delivery emails.                                                                                                                                                                    |
| **Email license key to buyer** | Whether the Commerce worker sends a "here's your license key" email after purchase. Defaults to on. Turn off if you'd rather fulfill via the `license.created` webhook. See [Commerce → Buyer delivery email](/features/commerce#buyer-delivery-email). |
| Display name                   | Header text shown in the portal (defaults to "License portal") and the buyer-facing line in delivery emails.                                                                                                                                            |
| Accent color                   | Hex color (`#A78BFA`) used for the portal primary button and highlights. A live swatch is shown in the dashboard.                                                                                                                                       |

When a user attempts a reset, the policy engine (`evaluateResetPolicy`) walks the license’s reset history and enforces limits before the reset is allowed. Every reset is then recorded via `recordPortalReset`, so the next attempt sees an up-to-date history.

## Branding

Branding is applied dynamically. When a user signs into the portal, the portal fetches the policy for their license’s app and:

* Swaps the MUI theme to use your accent color.
* Replaces the header title with your display name.

The login screen itself is generic; branding appears once the user proves ownership of the license. This avoids leaking which app a license belongs to before authentication.

## Hosting

The portal is hosted by AuthForge:

* **UI**: `https://portal.authforge.cc` (static Vite app, served via AWS Amplify).
* **API**: `https://portal-api.authforge.cc` (HttpApi behind a Lambda, deployed by our PortalStack).

You do not need to buy a domain or stand up any infrastructure. The portal works for every AuthForge app as soon as policy is published and licenses have emails attached.

## What the user sees

After signing in, the user lands on their license home page with:

* The license key (truncated), status, expiration, and attached email.
* Bound HWIDs and how many slots remain.
* A **Reset HWIDs** button, gated by policy. When the user is over their limit or inside a cooldown, the button is disabled with a clear explanation and (optionally) a link to your support URL.

## Related features

* **[Commerce](/features/commerce)**: Commerce-created licenses are portal-ready automatically.
* **[HWID locking](/features/hwid-locking)**: Portal resets clear the bindings the SDK maintains.
* **[Licenses API](/api/licenses)**: Attach or change emails programmatically.

## Security notes

* Portal session tokens are HMAC-SHA256 signed; the secret lives in AWS Secrets Manager (`PortalSessionSecret`).
* Magic codes are 6 digits, single-use, and expire inside a short TTL.
* Every user action (login, verify, reset) is logged with a request ID for support traceability.
* The email lookup is case-normalized and trimmed; the comparison itself is constant-time.
