# Branding

> Reference for the nauthera.io/v1alpha1 Branding resource, which themes the hosted sign-in pages for one OAuth client and is applied by the Nauthera operator.

A `Branding` resource themes the [hosted sign-in pages](https://www.nauthera.io/docs/v1/login-experience) for a
single OAuth client: the product name, logo, favicon, links, layout and a full
design-system theme. The [operator](https://www.nauthera.io/docs/v1/operator) serializes `spec.branding` to
JSON and writes it to the server's gRPC `AdminService` (`UpsertBranding`). The server
checks it, stores it and serves it on the sign-in pages for that client, laid over the
[organization's default branding](https://www.nauthera.io/docs/v1/organization-resource) field by field.

| Property | Value |
|---|---|
| API group / version | `nauthera.io/v1alpha1` |
| Kind | `Branding` |
| Plural / short name | `brandings` / `brand` |
| Scope | Namespaced |
| Definition | `deploy/crds/branding-crd.yaml` in nauthera-server |

## Example

```yaml
apiVersion: nauthera.io/v1alpha1
kind: Branding
metadata:
  name: example-webapp-branding
  namespace: nauthera
spec:
  clientId: example-webapp
  branding:
    productName: Acme
    logoUrl: https://acme.example/logo.svg
    faviconUrl: https://acme.example/favicon.svg
    mode: system
    supportUrl: https://acme.example/support
    fonts:
      - https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap
    theme:
      color:
        accent: "#7B61FF"
        neutralStyle: cool
      typography:
        scale:
          base: 14
          ratio: 1.2
        body:
          family: Inter
          fallbacks: "-apple-system, sans-serif"
      radius:
        base: 8
        multiplier: 1
```

`kubectl get brandings` shows the client ID, the phase and the `Ready` condition's
status and reason; `-o wide` adds the message.

The operator's identity needs the `brandings` operation, and the organization and
namespace, in its `server.grpc.tls.allowed_clients` entry. The Helm chart's default entry
grants it in the main organization from every namespace.

## Gallery

Each tab is one of the example brandings in nauthera-server's sign-in Storybook
(`web/packages/theme/src/presets.ts`), shown on the sign-in screen in light and dark mode
with the `Branding` resource that produces it. The pictures and the documents are captured
from the same build, so the manifest is exactly what the picture wears. Inline hero images
are shortened here. For a step-by-step walk through designing a theme in the Astryx
playground and applying it, see [Customize the sign-in UI](https://www.nauthera.io/docs/v1/customize-branding).

<BrandingGallery />

## What the operator does

- **Keyed by client.** `spec.clientId` names the client, in the organization the
  resource's namespace configures (`spec.organization` where several admit it). The
  server refuses a change to either, so moving a branding to another client is a delete and a new resource. The
  client does not have to be declared from a manifest, or exist yet: the pages served for
  that `client_id` wear the document once it does.
- **Writes against the revision it last wrote.** Each write names `status.etag`, the
  server's revision the resource last recorded. A document changed or removed over the
  API since is not written over: the resource goes to `Error` under `Drifted`, naming
  the revision the server holds. Annotate the resource `nauthera.io/restore` (any value)
  to write the declared document back; the operator removes the annotation when it has.
  The change is noticed on the operator's next write, that is, when the manifest changes.
- **Reports a refused document with its field.** The resource goes to `Error` under
  `Refused`, and the message names the field.
- **Removes the document** when the resource is deleted, and the client's pages fall back
  to the organization's default. There is no `deletionPolicy`: a branding decorates one
  client's pages and costs nothing to recreate. A finalizer (`nauthera.io/branding`)
  holds the resource until the server has removed it; until then the resource is in
  phase `Deleting` with reason `BrandingNotRemoved`. The hourly orphan sweep removes a
  branding this operator wrote whose manifest went without the finalizer running.

## Validation

The resource definition types the whole document. `theme` is the design system's own
theme by its names: `kubectl explain branding.spec.branding.theme` lists every axis with
its enums and bounds, and a field the design system does not know, or a value outside
those, is refused when the resource is applied. The token, custom-property and component
maps keep their values as written, bounded only in number. The server then checks each
of those values against the design system's contract and refuses one the sign-in pages
could not render, naming the field.

The server also refuses a document that is not a JSON object or is larger than 128 KiB.
Every `fonts` entry must be an https stylesheet on `fonts.googleapis.com` or
`fonts.gstatic.com`, and at most 8 are allowed: a stylesheet can restyle anything on the
page, the password field included, so the font CDN is the only host trusted with one.
`logoUrl`, `faviconUrl` and `hero.imageUrl` must be an https URL or an inline
`data:image/` URI of a type the pages show (PNG, JPEG, GIF, WebP, SVG or ICO) whose bytes
match that type. `supportUrl`, `termsUrl` and `privacyUrl` must be http or https URLs.

The resource definition holds the URL fields, the https scheme of a font and the font
limit to the same rules at apply time. It does not check a font's host or the 128 KiB
total, so a font from another host, or a document over the total, is applied and then
reported as `Refused`, with the message naming `fonts` for a font.

## Spec

{/* AUTO-GENERATED-SPEC-START */}

### `spec.branding`

**Type:** `object` | **Required**

The auth UI branding document. Chrome fields drive screen UI;
`theme` is the design system's theme (color, typography, radius,
motion, tokens, components, …). The operator serializes this
object to JSON and sends it as BrandingSpec.branding_json.

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `appName` | `string` | No | `—` | App name, shown as "Continue to \{appName\}". |
| `companyName` | `string` | No | `—` | Copyright holder shown in the footer. |
| `faviconUrl` | `string` | No | `—` | Icon shown in the browser tab and bookmarks: an https URL or an inline data:image/ URI. |
| `fonts` | `[]string` | No | `—` | Font stylesheet URLs to load, on fonts.googleapis.com or fonts.gstatic.com only: a stylesheet can restyle the password field, so the font CDN is the one host trusted with it. |
| `hero` | `object` | No | `—` | Hero pane content for the split layout. |
| `layout` | `string` | No | `—` | Page layout — centered (default) or split. One of: `centered`, `split`. |
| `logoUrl` | `string` | No | `—` | Logo image rendered in the header: an https URL or an inline data:image/ URI. The /ui CSP img-src blocks http images (mixed content). The admin dashboard shows an inline logo of up to 64 KiB, or one on its own origin, and nothing else. The organization's email shows its logo too; Gmail and Outlook show no SVG, so a PNG suits both. |
| `mode` | `string` | No | `—` | Default color mode. One of: `system`, `light`, `dark`. |
| `privacyUrl` | `string` | No | `—` |  |
| `productName` | `string` | No | `—` | Tenant/product name shown in the auth header. |
| `supportUrl` | `string` | No | `—` |  |
| `termsUrl` | `string` | No | `—` |  |
| `theme` | `object` | No | `—` | The design system's theme, as its defineTheme takes it. A field the design system does not know is refused; the values of its token and component maps are stored as written and checked by the server. |

### `spec.clientId`

**Type:** `string` | **Required**

The OAuth2 client_id this branding applies to.

### `spec.organization`

**Type:** `string` | **Optional**

The slug of the organization this branding belongs to. Needed only
where more than one organization admits the namespace in its
spec.namespaces; otherwise the one that does is taken. The main
organization is "default". Immutable.

{/* AUTO-GENERATED-SPEC-END */}

## Status

`phase` is one of `Pending`, `Ready`, `Error` or `Deleting`. The `Ready` condition's
reason is `Registered` when the server holds the current generation, `Drifted` or
`Refused` as above, `NamespaceNotCovered` when the resource's namespace is not in the
operator's `--namespaces`, `BrandingNotRemoved` while a deletion waits for the server, or
one of the connection and namespace reasons every resource shares (`NamespaceNotAdmitted`,
`OrganizationAmbiguous`, `OrganizationNotReady`, `OrganizationChanged`; see the
[OIDCClient reference](https://www.nauthera.io/docs/v1/oidc-client#status)). A `Restored` event records a
document written back over a drifted one.

{/* AUTO-GENERATED-STATUS-START */}

| Field | Type | Description |
|---|---|---|
| `clientId` | `string` | The resolved client_id the branding is bound to. |
| `conditions` | `[]object` | Standard Kubernetes conditions (Ready, Synced, …). |
| `etag` | `string` | The server's revision of this resource, returned on every write and read. A controller compares it against what it last wrote to tell its own change from one made elsewhere, and sends it back as the expected revision so a write that would overwrite someone else's is refused instead. |
| `issuer` | `string` | The issuer the server homed the branding at, after its default was applied: the organization whose sign-in pages wear it. |
| `lastReconciledTime` | `string` |  |
| `observedGeneration` | `integer` | .metadata.generation last reconciled by the server. |
| `organization` | `string` | The slug of the organization the server holds the branding in. Once set, a namespace that comes to configure another organization is refused rather than moving the branding. |
| `phase` | `string` |  |

{/* AUTO-GENERATED-STATUS-END */}
