# Desktop and mobile app

A Récit site becomes a native application with one `app:` section and one command,
`recit app build`. The application embeds the site exactly as `recit-site build` produced it
(`dist/`) in a shared [Tauri 2](https://tauri.app) shell, and serves it offline. Nothing is added to
the site: the pages do not know they run in a window.

:::note{title="Work in progress"}

The `app:` section is in `@recit/core`; `@recit/app` and the `recit app` commands are being built
(desktop first, then [Android](/documentation/configuration/app-android) and
[iOS](/documentation/configuration/app-ios)). This page is their reference.

:::

## Install

```bash
pnpm add -D @recit/app
```

`@recit/app` carries the shell and `@tauri-apps/cli`; `recit` loads it on demand, as it loads
`@recit/motion-render` for `recit render`. Without it, `recit app` stops with "install
`@recit/app`". The [CLI](/documentation/reference/cli) comes from `@recit/cli`, as usual.

## The `app:` section

A Récit section of [recit.config](/documentation/configuration/recit-config#a-site), next to
`publish`. Only `identifier` has no safe default when the site has no `canonicalUrlOrigin`.

```ts title="recit.config.ts"
import "@recit/site-stories/styles.css";
import { defineConfig } from "recit/config";

export default defineConfig({
  // Site options
  site: { title: "Lorem Ipsum Docs" },
  canonicalUrlOrigin: "https://docs.example.com",
  navigation: [{ type: "doc", file: "index" }],
  // Récit sections
  publish: { dir: "./stories", routePrefix: "/stories" },
  app: {
    productName: "Lorem Ipsum Docs",
    identifier: "com.example.loremipsum",
    window: { width: 1280, height: 800, minWidth: 720 },
  },
});
```

| Key                                   | Default                                                                        | Meaning                                                                                                                                                             |
| ------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `productName`                         | `site.title`, else the name of the site folder                                 | name of the application and of its window                                                                                                                           |
| `identifier`                          | `canonicalUrlOrigin` reversed: `https://docs.example.com` → `com.example.docs` | reversed domain, at least two segments of letters, digits and underscores (no hyphen: Android rule). **Final once published**: the stores and the updater key on it |
| `version`                             | `version` of the site's `package.json`, else `0.0.0` with a warning            | the version the system shows                                                                                                                                        |
| `icon`                                | `metadata.favicon` of the site                                                 | square source, PNG of 1024 px or more or SVG, relative to the site; every platform size is derived from it                                                          |
| `window.width`, `window.height`       | `1280`, `800`                                                                  | size of the window at first launch                                                                                                                                  |
| `window.minWidth`, `window.minHeight` | none                                                                           | minimum size                                                                                                                                                        |
| `window.resizable`                    | `true`                                                                         | —                                                                                                                                                                   |
| `allowedOrigins`                      | `[]`                                                                           | origins the window may load besides the bundle; every other link opens in the system browser                                                                        |
| `deepLink`                            | none                                                                           | URL scheme that opens the application (`lorem-docs://…`)                                                                                                            |
| `android.minSdkVersion`               | `24` (Android 7.0)                                                             | see [Android](/documentation/configuration/app-android)                                                                                                             |
| `ios.developmentTeam`                 | none                                                                           | Apple team of the signature; see [iOS](/documentation/configuration/app-ios)                                                                                        |
| `updater.endpoints`, `updater.pubkey` | none                                                                           | automatic updates on desktop, opt-in; see [Updates](#updates)                                                                                                       |

### Names

The window shows `productName` exactly as written. The files on disk follow two rules of the
systems:

- the **Linux packages** (`.deb`, `.rpm`) are named after the ASCII form of `productName`, because
  Debian and RPM refuse a package name outside ASCII: `Récit Docs` gives `recit-docs`, and the
  diagnostic `app-product-name-ascii` says so. A name with nothing left in ASCII takes the name
  below;
- the **binary** is named after the identifier without its first segment, in lowercase with hyphens
  (`mainBinaryName` of Tauri): `com.example.loremipsum` gives `example-loremipsum`, so two
  applications installed on the same machine never share `/usr/bin/recit-app`.

### Diagnostics

`recit app` resolves the section before anything else and prints the problems like `recit build`. An
error stops the build; a warning does not.

| Code                     | Severity | When                                                                                                                          |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `app-missing`            | error    | no `app:` section; the message gives the minimal section to add, with the computed defaults                                   |
| `app-identifier-missing` | error    | neither `app.identifier` nor `canonicalUrlOrigin`                                                                             |
| `app-identifier-invalid` | error    | an identifier that breaks the rule above (a hyphen, one segment, a digit first)                                               |
| `app-base-path`          | error    | the site sets `basePath`: the application serves the site at its root, the router and the search index expect the prefix      |
| `app-cdn-url`            | error    | the site sets `cdnUrl`: the assets would come from the network                                                                |
| `app-version-missing`    | warning  | no `app.version` and no `version` in the site's `package.json`; `0.0.0` is used (Android refuses it)                          |
| `app-product-name-ascii` | warning  | `productName` has characters outside ASCII: the packages take its ASCII form, the binary the identifier (see [Names](#names)) |
| `app-search-remote`      | warning  | `search.type: "inkeep"`: that search calls a remote service; Pagefind works offline                                           |
| `config-invalid`         | error    | the `recit.config` itself is invalid, one diagnostic per problem                                                              |

## Commands

| Command                                       | Options                                               | Effect                                                                                                                      |
| --------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `recit app build [dir]`                       | `--target desktop\|android\|ios`, `--skip-site-build` | `recit-site build` in the site, then the native build; the installers are copied to `.recit/app/bundle/`                    |
| `recit app dev [dir]`                         | —                                                     | reuses a `recit-site dev` already listening on the site's port, otherwise starts one, then opens the window on it, with HMR |
| `recit app preview [dir]`                     | `--port <port>`                                       | serves `dist/` in the browser exactly as the application resolves it (deep reload, 404 fallback), without Rust              |
| `recit app android <init\|dev\|build> [-- …]` | arguments after `--` go to `tauri android …`          | see [Android](/documentation/configuration/app-android)                                                                     |
| `recit app ios <init\|dev\|build> [-- …]`     | arguments after `--` go to `tauri ios …`              | see [iOS](/documentation/configuration/app-ios)                                                                             |
| `recit app icon [source]`                     | —                                                     | regenerates the icons of every platform from `app.icon`, or from the given file                                             |

`dir` defaults to the current folder. Everything generated lives in `<site>/.recit/app/`: the shell,
its `tauri.conf.json` (generated at every call, never edited by hand), the icons, the Cargo
`target/` and, after a build, the installers in `bundle/`. The folder is ignored by git.
`RECIT_APP_TARGET_DIR` moves the Cargo cache out of the site, to share it between the sites of a
monorepo: the second site does not compile Tauri again, and its installers are still copied to its
own `.recit/app/bundle/` (Tauri writes them under the shared target directory, where the next build
of another site overwrites them).

A site of a pnpm workspace adds the command to its `package.json`:

```json title="package.json"
{
  "scripts": {
    "build": "recit-site build",
    "app:build": "recit app build"
  }
}
```

## Prerequisites (desktop)

Rust stable, installed at user level with [rustup](https://rustup.rs), and the libraries of the
system's webview:

- **Linux** (Debian, Ubuntu):
  `sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev libssl-dev libxdo-dev pkg-config file curl wget`.
  A machine without a display links the application all the same; to start it there (a smoke test),
  add `xvfb`, `xauth` and `dbus`, and run it under `xvfb-run -a dbus-run-session -- <binary>`:
  without a session bus, GTK waits 25 seconds before the window appears.
- **macOS**: Xcode Command Line Tools (`xcode-select --install`).
- **Windows**: Visual Studio Build Tools with the C++ workload; WebView2 is part of Windows 10
  and 11.

The first build compiles the shell and its dependencies, a few minutes; the next ones reuse the
Cargo cache.

## What works offline

The bundle of `recit-site build` has everything the site needs, so the application does too:

- the pages, one HTML file per route, and the client router after the first one;
- the Pagefind search, index and WebAssembly included;
- the fonts, the code highlighting, the math and the diagrams, all in the bundle;
- the stories the site [publishes](/documentation/integrations/site-stories): decks and films play
  in the window.

A reload on a deep page finds its HTML file; an unknown address falls back on `index.html`, so the
site's 404 page shows. `recit app preview` reproduces that resolution in the browser, to check a
site before installing Rust.

## Sizes

Measured on the example site `examples/site-embed` (a few pages, one deck, Pagefind), built on Linux
x86_64 with Tauri 2.12:

| Artefact       | Size     | Note                                                                       |
| -------------- | -------- | -------------------------------------------------------------------------- |
| binary         | 25 MB    | the shell and the whole `dist/`, embedded                                  |
| `.deb`, `.rpm` | 13.4 MiB | compressed; the system provides WebKitGTK                                  |
| `.AppImage`    | 105 MiB  | carries WebKitGTK and GTK, so it runs on any distribution                  |
| Android `.apk` | 21.7 MB  | universal, signed; see [Android](/documentation/configuration/app-android) |
| Android `.aab` | 15.2 MB  | the store builds a smaller `.apk` per device from it                       |

A site grows these numbers by the size of its `dist/`: pages, images, fonts and the search index.

## Limits

- `basePath` and `cdnUrl` are refused (see the diagnostics): the application serves the site at its
  root, from its own files.
- No server in the application: no SSR, no API. A site built with `--experimental-ssr` is a hosting
  matter.
- Links to other origins open in the system browser, the window stays on the site. To load another
  origin inside the window, list it in `allowedOrigins`.
- The [smartphone remote](/formats/deck/smartphone-remote) of a deck needs the development server:
  there is none in the application. The [presenter console](/formats/deck/presenter-console) opens
  in a second window.
- `recit render` (MP4) stays a Node and Chromium pipeline, outside the application.

## Updates

On desktop, updates are opt-in: the `updater` section names the endpoints that serve `latest.json`
and the public key that signs the binaries (`tauri signer generate`; the private key stays a secret
of the release workflow). Without the section, the shell ships without the updater. On Android and
iOS, the stores update the application; `updater` is ignored there.
