Ctrl K
ComunidadGuías…

docs/adr/0090-legal-surface-and-launch-gate.md

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

Archivo original ↗Preguntar a KiBot
# ADR-0090: The legal surface is server-owned data, and a launch gate guards it

## Metadata

| Field          | Value                                                     |
| -------------- | --------------------------------------------------------- |
| **Status**     | Accepted                                                  |
| **Date**       | 2026-10-19                                                |
| **Authors**    | Alessio Brillo                                            |
| **Deciders**   | Alessio Brillo                                            |
| **Supersedes** | —                                                         |
| **Relates to** | ADR-0025, ADR-0026, ADR-0027, ADR-0028, ADR-0030, ADR-0054, ADR-0083 |
| **Project**    | DOMINUS                                                   |

## Context

v1.2.0 shipped three legal pages in the SPA (`/terms`, `/privacy`,
`/impressum`). They were a good-faith first draft and they had three defects
that no test, no linter and no reviewer had caught:

1. **Wrong jurisdiction.** The notice was written for Germany ("Angaben gemäß
   § 5 TMG"). The operator of DOMINUS Cloud is an Italian economic operator,
   so the applicable rule is art. 7 of D.Lgs. 70/2003 (electronic commerce),
   not the German one — and the document named a legal regime the operator is
   not subject to.
2. **Placeholder identity.** The notice published `privacy@dominus.example`
   and no postal address. A legal notice with a placeholder is not a legal
   notice; it is worse than an absent one, because it looks compliant.
3. **Unreachable from the public surface.** The server-rendered public pages
   (`/public/domain/:domain`, `/public/for-sale/:domain`, `/public/compare/…`)
   carried no legal link at all. Art. 7 asks for *immediate* reachability
   from every page, not a link hidden behind a terms page.

The same release left three commercial obligations unmet, all recorded in
`docs/releases/cloud-launch-readiness.md`: no VAT handling beyond
`automatic_tax`, no data-subject export/erasure path, and no mechanical
answer to "may this deployment start selling?".

## Decision Drivers

1. **The operator identity is data, not code.** It changes when the operator
   changes, it must be identical on the SPA and on the server-rendered
   pages, and it must never require a code change to publish.
2. **The community edition is not the Cloud.** In a self-hosted deployment the
   deployer is the controller and the AGPL governs the software; publishing
   Cloud commercial terms there would be misleading, and publishing the
   operator's identity there would be wrong.
3. **A missing legal clause is invisible.** No reviewer notices a missing
   withdrawal clause, and no test fails because of one. The checks that
   matter here must be mechanical.
4. **The documents must be translatable later.** The i18n increment planned
   for v1.4.0 must be able to translate the legal text without rewriting
   components.
5. **No formal legal review is available now.** The decision to publish
   unreviewed text is a decision that must be written down, with its
   consequences, rather than an omission.

## Considered Options

### Option A: Keep the legal pages as React components

**Advantages:** no backend work, no public endpoint, the text lives next to
the components that render it.

**Disadvantages:** the operator identity becomes a build-time constant (a
release to change an address); the server-rendered pages cannot show the same
text without duplicating it in the view layer, and duplication is how the two
copies drift; translation would mean editing three components.

### Option B: The legal surface is server-owned structured data

The backend owns the documents and the identity, assembles them from the
configuration, and serves them as structured data. The SPA renders the
structure; the server-rendered pages render the identity in their footer.

**Advantages:** one source of truth; the identity is configuration; the
public pages and the SPA cannot drift; translation becomes a catalogue
problem, not a component problem; the guardrail can inspect the published
text.

**Disadvantages:** a public endpoint exists; the frontend depends on it.

### Option C: Publish the legal documents on an external static site

**Advantages:** no code at all.

**Disadvantages:** the identity and the terms would live outside the product,
so the footer of every public page would link to another origin — and the
product could not be self-hosted without pretending to be someone else. It
also means the terms of a self-hosted deployment are decided by the operator
of a site the deployer never sees.

## Decision

**Option B.**

- `src/services/legal-content.ts` holds the three documents as structured
  data (title, scope, sections with paragraphs and bullets) and assembles
  them per deployment mode. `src/services/legal-identity.ts` reads the
  identity from the configuration and decides whether it is complete.
- The identity comes from the `LEGAL_*` environment variables
  (`LEGAL_ENTITY_NAME`, `LEGAL_REGISTERED_ADDRESS`, `LEGAL_VAT_ID`,
  `LEGAL_REA_NUMBER`, `LEGAL_PEC`, `LEGAL_CONTACT_EMAIL`,
  `LEGAL_PRIVACY_EMAIL`, `LEGAL_SUPERVISORY_AUTHORITY`). They are optional:
  a self-hosted deployment publishes documents that describe the deployment,
  not the DOMINUS operator.
- The documents are served unauthenticated at `GET /public/legal` and
  rendered by the SPA at `/legal-notice`, `/terms` and `/privacy`. The German
  `/impressum` route redirects to `/legal-notice`.
- Every server-rendered public page carries the legal footer
  (`src/api/views/legal-footer.ts`): the operator identity and the links to
  the three documents. The footer context is injected once at boot, because
  the alternative — a parameter on five renderers — makes it easy to forget
  one page.
- The community edition and the cloud edition publish **different**
  documents. The commercial terms (payments, withdrawal, SLA, liability
  caps) exist only in the cloud documents; the community documents describe
  the AGPL licence and the self-hosted data flow.
- `scripts/check-legal-placeholders.mjs` runs in the gate and fails when:
  a placeholder appears in the published text; the notice stops rendering an
  identity field that art. 7 requires; a required clause (withdrawal,
  reverse charge, sub-processors, supervisory authority) disappears; the
  public pages stop carrying the footer or the SPA stops registering a
  route; an identity variable leaves the configuration or `.env.example`.
- The runtime counterpart is the launch gate (`npm run launch:check`),
  which runs the same checks against a live deployment with the real
  configured values.

### The texts are drafts, and that is a recorded decision

No formal legal review has been performed on the three documents. They were
drafted against the applicable rules (art. 7 D.Lgs. 70/2003; the Consumer
Code for withdrawal, art. 52 and 59; art. 13/14/28 GDPR; the VAT rules for
reverse charge and OSS) and they are published in English, with the Italian
version stated as prevailing.

The consequences are accepted for this release and are not hidden:

- the launch gate refuses a deployment whose identity is missing or still
  holds a placeholder, so the failure mode is "not launchable", not
  "publishing a placeholder";
- the paid tiers stay behind the existing fail-closed billing gate
  (`BILLING_NOT_CONFIGURED` when Stripe is not configured) until the
  operator has a fiscal position — enabling collection without one would be
  a business decision, not an engineering one;
- a formal review remains open work, tracked next to the third-party
  security audit.

## Consequences

### Positive

- The public pages identify the operator, as the law requires, and the
  identity is a configuration change rather than a release.
- A regression in the legal surface fails the gate instead of waiting for a
  reader to notice.
- The v1.4.0 i18n increment translates a catalogue, not a component tree.
- A self-hosted deployment no longer shows an operator identity that does not
  exist for it.

### Negative

- A public endpoint exposes the operator identity and the full text of the
  documents. That is the point of the requirement, and the documents contain
  nothing that is not published on the same pages anyway.
- The legal text lives in the repository, so it is versioned, diffed and
  reviewed in the open. For an unpublished draft that is the desired
  transparency; once the text is validated by a lawyer, the operator may
  prefer to move it out of the repository. That is a deliberate later
  decision, not an oversight.
- English only. German and Italian versions are deferred to the i18n
  increment, with the English text prevailing in the meantime.

## Links

- `src/services/legal-content.ts` — the documents.
- `src/services/legal-identity.ts` — the identity and its completeness.
- `src/api/views/legal-footer.ts` — the footer of the public pages.
- `scripts/check-legal-placeholders.mjs` — the repository guardrail.
- `docs/releases/cloud-launch-readiness.md` — the launch blockers and the
  order in which they must be cleared.