Ctrl K
ComunidadGuías…

docs/licensing.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# How licensing works

Written so that in six months you can answer "how does someone actually buy this"
without reading the code.

## The short version

A Pharos licence is **a signed sentence, not a database record.** There is no licence
server to call, nothing to check, nothing that can go down.

```
key = base64url(payload) . "." . base64url(signature)

payload = {"product":"pharos","issued_to":"...","features":["brand_pack"],"issued_at":"..."}
```

The customer pastes that string into **Admin → Branding**. Pharos verifies the signature
against a public key compiled into the app and unlocks the features listed in the payload.
That is the whole mechanism.

**The private key is the entire product.** Anyone holding it can mint licences. Anyone
without it cannot forge one, no matter what they patch: they would have to change the
public key in their own copy, which is legal under the AGPL and also means they are no
longer running your build.

## Plans

| Plan | Price | What the key carries | Pages |
|---|---|---|---|
| Free | none | no key | 1 |
| Brand pack | € 79, one time | `brand_pack`, no `expires_at` | 1 |
| Supported | € 149 per year | `brand_pack`, `multi_pages`, `limits.status_pages` = 5, `expires_at` one year out | up to 5 |
| Commercial licence | from € 599 per year, by quote | `brand_pack`, `multi_pages`, no page limit, `expires_at` one year out | unlimited |

- **Free**: every feature of the core, 1 status page, the "Powered by Pharos" footer credit.
- **Brand pack**: own logo (light and dark), favicon, logo in email and editable mail
  templates, footer credit removed. Keeps working forever.
- **Supported**: the Brand pack included and kept, email support, and up to 5 status pages, each with its own components, subscribers, branding, email,
  integrations and user roles per page.
- **Commercial licence**: everything in Supported, unlimited status pages, and the AGPL
  publication duty lifted (that part is the written agreement, not the key).

When a yearly key lapses, `brand_pack` stays (it is in `License::PERPETUAL`) and existing
pages keep running, but creating or reactivating pages beyond 1 is blocked.

Signing a Supported key by hand:

```bash
php artisan pharos:license:sign customer@example.net \
  --features=brand_pack,multi_pages --status-pages=5 --months=12 \
  --domain=status.customer.example --key=/root/secrets/pharos-license-secret.hex
```

For a commercial key, leave `--status-pages` off (no limit means unlimited).

The portal plans (`/account/buy/brand-pack`, `/account/buy/supported`) must sign keys with
these features and limits; prices stay as Stripe price IDs in the portal config. The
commercial licence is quoted individually and its key is signed by hand.

## The keys

| | |
|---|---|
| Secret (signing) | `/root/secrets/pharos-license-secret.hex` (**never leaves our side**) |
| Public (verifying) | `/root/secrets/pharos-license-public.hex` → `PHAROS_LICENSE_PUBLIC_KEY` in every install |

The same pair signs **release manifests** for the updater. A `purpose` field keeps the two
apart, so a licence key can never be replayed as a fake update and the other way round.

If the secret ever leaks: generate a new pair, ship a release with the new public key, and
re-issue keys to existing customers. Old keys stop verifying on the new version only;
nobody's site breaks.

## Issuing a key by hand

For a commercial licence, or a customer who paid by invoice:

```bash
php artisan pharos:license:sign customer@example.net \
  --features=brand_pack \
  --key=/root/secrets/pharos-license-secret.hex
```

Prints the key. Mail it. Done.

Features currently understood by the app: `brand_pack` and `multi_pages`. Everything else in the payload is
ignored, so adding a future feature name to a key is harmless.

## Issuing a key after a payment

The portal (`/account`) does this automatically:

1. Customer clicks **Buy** on the pricing table → `/account/buy/{plan}`
2. The portal creates a Stripe Checkout session for the price ID in its config
3. Stripe redirects the customer, takes the money, and posts
   `checkout.session.completed` to the portal's webhook
4. The portal verifies the Stripe signature, records the customer, signs a licence key
   and emails it
5. The customer can log in later with a magic link to fetch the key again

Prices live in the portal's config as Stripe **price IDs**, not amounts. Changing a price
in Stripe therefore does not require a code change, and the pricing table on the website
never links to a Stripe URL that can go stale.

## What the customer sees when it is wrong

Every failure reads as *not licensed*, never as an error:

- Malformed key → not licensed
- Valid signature, wrong product → not licensed
- No key at all → not licensed

A status page must not go down because a licence check had a bad day. That rule is in
`App\Services\License::verify()` and there is a test for it.

## Offline operation and page licences

No phone-home is required. Licence rights are verified locally. Keys may include
expiry and a central-installation domain binding; see the sections below.

Multiple pages use the signed `multi_pages` feature and optional positive integer
`limits.status_pages`. Supported keys carry a limit of 5; commercial keys carry no limit.
Both also include `brand_pack`. Creation
and reactivation above the active-page limit are blocked; existing pages keep running.
See [multiple status pages](multiple-status-pages.md) for signing examples and behavior.

## Keys that run out

A key may carry `expires_at`. Sign one with a term:

```bash
php artisan pharos:license:sign klant@example.net --features=brand_pack --months=12
```

A key may carry `issued_for`, the status page hostname it was sold for; Pharos refuses it on any
other host (case-insensitive, a leading `www.` is ignored). The portal fills it from the domain
typed at checkout. Sign one by hand with:

```bash
php artisan pharos:license:sign klant@example.net --features=brand_pack --domain=status.klant.nl
```

Leave `--months` off and the claim is absent, which means the key never expires.
That is what every key signed before this existed does, and they keep working.

Expired keys lose time-limited rights such as creating extra pages. Brand Pack is
a perpetual feature and remains usable. Existing status pages continue operating.
The branding screen shows the expiry date and warns for the last thirty days.

There is still nothing to revoke a key that is already out there. That is the
price of checking offline, and expiry is the answer: a key that was passed around
runs out on its own.