# Cloud & teams

A built deck is one self-contained `.html` you can host anywhere. **liebstoeckel cloud**
(the hosted control plane) adds a private, always-on home for your decks behind a login.
For teams, it adds a shared library with members, roles, and invitations.
**Coming soon:** liebstoeckel cloud is not generally available yet. The CLI already ships the cloud
commands (`login`, `push`, `orgs`, `decks`, `brand`), but there is no public hosted
service to sign in to. Everything on this page describes the hosted layer as it will
launch; the rest of the framework works fully offline today.
**Note:** The framework is MPL-2.0. The cloud is the operated layer; the CLI talks to it over a
versioned REST API, so it can also point at a private deployment of the control plane.

## Sign in from the terminal

Authentication is passwordless. `login` runs the OAuth 2.0 device-authorization grant:
the CLI prints a URL, you open it, sign in with a one-time email code, and approve.

```bash
liebstoeckel login --api https://<your-control-plane-host>
```

The token is stored in `~/.config/liebstoeckel/credentials.json` (mode `600`).

## Push a deck

```bash
liebstoeckel build          # → ./dist/<deck-slug>.html
liebstoeckel push           # uploads the built deck in ./dist
```

The deck's **title** comes from its embedded `<title>` (or an explicit `--title`
override), and its **cover thumbnail** is read from the slide thumbnails already baked
into the file, so there is nothing extra to send. The server reads both straight out of
the uploaded file, which means titles with em-dashes, smart quotes, or emoji just work.
The deck appears in your dashboard immediately.

## Organizations: personal and teams

Every account has a **personal workspace** (just you) and can create or join **team
organizations**. A deck always belongs to exactly one organization, the *active* one.

- In the **dashboard**, switch the active workspace with the switcher in the top-left.
- From the **CLI**, list your workspaces and choose where `push` lands:

```bash
liebstoeckel orgs            # list workspaces; → marks the push default
liebstoeckel orgs use acme   # make the "acme" team the default
liebstoeckel push --org acme # …or target a team for one push
```

## Teams: shared library, members & roles

Every deck pushed to a team is visible to **all its members** as a shared library, with
"uploaded by" attribution. Who can do what is governed by three roles:

| Role | Can |
| --- | --- |
| **owner** | everything, incl. manage any deck, manage members, delete the org |
| **admin** | manage any deck, invite/remove members, change roles |
| **member** | upload decks; manage **their own** decks; view all team decks |

Manage all of this on the dashboard's **Team** page:

- **Invite a colleague** by email. They get a link, sign in with a one-time code, and join
  automatically (still passwordless).
- **Change roles** or **remove** members.
- **Leave** a team you no longer need (your personal workspace always stays).

## Share a deck publicly

Any deck can be published to a time-limited public link served on a separate domain, with
provenance chrome and an abuse-report path. Open a deck's **share** action in the dashboard
to set:

- **Expiry**: how long the link lives (7 days on the free plan; longer, or *no expiry*, on
  paid).
- **White-label** *(paid)*: drop the "Published with liebstoeckel" banner. The abuse-report
  path always remains.

## Present live from the dashboard

Turn any hosted deck into a **live, interactive session** straight from the browser, with
no CLI and no local runtime. Open a deck's **● present live** action and **Start live
session**:

- A **presenter view** opens (you drive the slides), and a **shareable audience link**
  appears that you can copy or hand out. Your audience opens it on any device and follows
  along, voting in polls, asking questions, and reacting in real time.
- Press **End session** when you're done. The audience link stops working immediately.

The deck runs in an isolated sandbox for the audience, and the relay that carries the
session **never executes deck code**, the same isolation as a [public relay](https://docs.liebstoeckel.app/guides/relay/).
Audience members can only interact through plugins (votes, questions, reactions); they can't
drive navigation or tamper with results.

**Free** gives you a real, usable live session with a capped audience and duration, and
**results aren't kept** once the session ends. **Pro / Team** raise the limits, drop the
audience watermark, and **persist results**: open a past session under **present live** to
see decoded poll tallies, the Q&A backlog, and reaction totals.
**Note:** Built-in plugins (poll, Q&A, reactions) sync purely peer-to-peer over the shared document,
so hosted live presenting needs no server-side plugin code. Plugins with custom server logic
run in the presenter's own browser tab.

## Shared brand

Your org is an **authenticated registry**, speaking the same protocol as `liebstoeckel add`
but private to your team. A **brand** (a theme token set: colours, fonts, glow, and an
ordered **chart palette**) is its first item type; custom company components can ride the
same rails later.

- **Define it** once: push a `defineTheme(...)` file (`liebstoeckel brand push ./brand.ts
  --default`) or edit it visually in the dashboard's **Brand** page.
- **Use it**: `liebstoeckel brand pull <name>` writes `brands/<name>.ts` into a deck, and
  `liebstoeckel new` auto-applies the org **default** brand. The brand is **owned source,
  baked into the deck at build**: self-contained and WYSIWYG, never a serve-time surprise.
- **Update it**: change the brand centrally, then authors (or CI) **re-pull + rebuild**.
  Deliberate, not silent.

Shared brand is a Pro/Team feature.

### Generate a starting point

Not sure where to start? The Brand page's **Generate** button rolls a complete brand
(colours, fonts, glow, and chart palette) in one click. It is *constrained*, not random:
a single seed picks a base hue, a colour-harmony scheme (analogous / complementary /
triadic / …), and a font-pairing recipe (minimal / editorial / modern / …), then every
token is derived by rule in the perceptual **OKLCH** space, with each colour floored to a
legible contrast on the dark background. Fonts are drawn from the catalog, so a generated
brand always ships its webfonts on `pull`. Re-roll until you like it, then tweak any field
by hand. The generated values are an editable starting point, never locked in.

### Fonts

The Brand page's **Heading**, **Body**, and **Mono** fields are a picker over a curated
catalog of [Fontsource](https://fontsource.org) variable fonts (grouped sans / serif /
display / monospace), with a **Custom…** option for any other `font-family` you type. The
preview shows the real typeface as you choose.

When you `liebstoeckel brand pull <name>`, a catalog font comes with its webfont: the
generated `brands/<name>.ts` `import`s the matching `@fontsource-variable/*` package, and
`pull` runs `bun add` to install it, so the font is **inlined into your single-file deck at
build** with no CDN involved. `liebstoeckel new` does the same for the org default brand.
Pass `--no-install` to write the files and install the font packages yourself. A
**Custom…** font carries no package; supply its `@font-face` yourself, or it falls back to
the system font.

If you author a brand as a file instead (`brand push ./brand.ts`), any font family that
isn't in the catalog is accepted but **flagged with a warning** (with a *did you mean* when
it's close, e.g. `"Inter"` → `"Inter Variable"`), so the file path is as honest as the
picker about which fonts actually ship a webfont.

### Chart palette

A brand also carries an ordered **chart-series palette** (`viz`): the colours charts cycle
through, exposed as `--brand-viz-0..n`. Edit it on the Brand page (add / reorder / remove
swatches) or set `viz: [...]` in a pushed `defineTheme(...)`. It's baked into pulled decks
so charts match the brand, not just the page chrome.

## Versions

A deck has a **stable identity** across re-pushes. `push` keys a deck by its folder name
(or `--name`), so editing and pushing again **updates the same deck at the same URL**;
the share link and view counts carry over. `--new` starts a separate deck.

- **Free** keeps the **latest** version (a re-push replaces it; the URL and stats are
  preserved).
- **Pro / Team** keep **full version history** with one-click **rollback**: open a deck's
  **Versions** panel to see every version, preview one (`/d/:id?v=N`), or make an older
  version current.

## View analytics

Every deck shows **view counts** (total and unique), for both private opens and public
share links. Counts are privacy-preserving (no raw IP/UA is stored). Expand a deck to see a
**14-day history** strip on paid plans; the free plan shows counts only.

Public share links also carry a **view cap** on the free plan, a built-in guardrail against
abuse that doubles as the upgrade nudge; paid plans are uncapped.

## Plans

Workspaces are **free** by default. Paid plans (**Pro** / **Team**) unlock uncapped share
views, view history with longer retention, white-label shares, longer/no-expiry links, and
larger live sessions with **persisted results**.
Plans attach to the **organization** (one subscription per workspace). The CLI shows your
current plan under `liebstoeckel orgs`.

[CLI reference: login · push · orgs · decks](https://docs.liebstoeckel.app/reference/cli/#login)