> ## Documentation Index
> Fetch the complete documentation index at: https://docs.skyvexsoftware.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build & Deploy

> Build, bundle, and distribute your plugin.

Plugins are built with Vite using a shared config factory from the SDK. The output is zipped and uploaded to the Skyvex website for distribution.

## Vite Configuration

Use `createPluginConfig()` from the SDK:

```ts theme={null}
// vite.config.ts
import { createPluginConfig } from "@skyvexsoftware/stratos-sdk/vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default createPluginConfig({
  ui: { entry: "src/ui/index.tsx" },
  background: { entry: "src/background/index.ts" }, // optional
  vite: {
    plugins: [tailwindcss(), react()],
  },
});
```

The `vite` option accepts any Vite config to merge in — plugins, resolve aliases, CSS config, etc.

<Warning>
  `@vitejs/plugin-react` is **required**, not optional. The SDK injects a React Fast Refresh preamble into your entry file that imports from `/@react-refresh` — an endpoint only served when `react()` is registered. Without it, `pnpm dev` against a running Stratos will fail to load the plugin UI with a cross-origin module error.
</Warning>

If you only have a UI module, omit the `background` field.

## Build Scripts

With background module:

```json theme={null}
{
  "scripts": {
    "dev": "vite",
    "build": "vite build && cross-env BUILD_TARGET=background vite build",
    "build:ui": "vite build",
    "build:bg": "cross-env BUILD_TARGET=background vite build",
    "bundle": "pnpm build && stratos-deploy"
  }
}
```

UI-only plugin:

```json theme={null}
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "bundle": "pnpm build && stratos-deploy"
  }
}
```

Use `cross-env` for Windows compatibility with the `BUILD_TARGET` environment variable.

## How the Build Works

**UI build** (default): Compiles your React code into a single ESM bundle. Shared dependencies are externalised and resolved from the shell at runtime via `window.__stratos_modules__`.

**Background build** (`BUILD_TARGET=background`): Compiles your Node.js module into an ESM bundle. Electron, Node.js builtins, and the SDK are externalised.

### CSS and Static Assets

Plugins are loaded at runtime via `import()`, so CSS and assets must be embedded in the JS bundle — separate files won't be loaded.

**CSS injection** is handled automatically. Any CSS you import (including Tailwind output from `@tailwindcss/vite`) is injected into the DOM as a `<style>` tag when your plugin module loads. The shell wraps all plugin CSS in `@layer plugin` to prevent your styles from overriding the shell's UI — you don't need to do anything special for this.

Your plugin's `styles.css` should include Tailwind and a `@source` directive so Tailwind can find your component files:

```css title="src/ui/styles.css" theme={null}
@import "tailwindcss";
@source "./**/*.{ts,tsx}";
```

Import it in your plugin's entry file:

```tsx theme={null}
import "./styles.css";
import "./hero-banner.css";  // any additional custom CSS
```

**Static assets** (images, SVGs, fonts) under 10 MB are inlined as data URIs automatically. Import them as usual and use the result as a `src`:

```tsx theme={null}
import logo from "./assets/logo.svg";
import badge from "../assets/badge-icon.svg";

<img src={logo} alt="Logo" />
<img src={badge} alt="Badge" className="h-5 w-5" />
```

This works for any file type Vite can handle (`.svg`, `.png`, `.jpg`, `.webp`, `.woff2`, etc.).

### What Gets Externalized

You do **not** need to bundle these — the shell provides them at runtime:

**UI externals:**

* `react`, `react-dom`
* `@tanstack/react-query`, `@tanstack/react-router`
* `@skyvexsoftware/stratos-sdk`
* `sonner`, `socket.io-client`
* `maplibre-gl`, `react-map-gl/maplibre`

**Background externals:**

* `electron`
* `@skyvexsoftware/stratos-sdk` (except `/helpers`, which is bundled into your plugin)
* `socket.io-client`
* All Node.js built-in modules

Anything else you import (e.g. `lucide-react`, your own utilities) **will** be bundled into your plugin.

## Build Output

After `pnpm build`, the `dist/` directory contains:

```
dist/
├── plugin.json                ← copied from root
├── assets/                    ← icons (for sidebar display)
│   ├── icon-light.svg
│   └── icon-dark.svg
├── ui/
│   └── index.js               ← bundled UI module (ESM, CSS + assets inlined)
└── background/                 ← only if background module exists
    └── index.js               ← bundled background module (ESM)
```

CSS and imported static assets are embedded directly in `ui/index.js` — there are no separate `.css` files in the output.

***

## Development Workflow

### Live Reload Development

The recommended development workflow uses the Stratos app's [developer mode](/sdk/developer-mode):

1. Open Stratos with the `--dev` flag
2. Log in and select your airline
3. In your plugin directory:

```bash theme={null}
pnpm dev
```

Your plugin's Vite dev server auto-connects to Stratos via Socket.io. The plugin appears in the sidebar immediately. Code changes auto-reload in about a second.

```
✓ Vite dev server running at http://localhost:5174
✓ Connected to Stratos (localhost:2066)
✓ Plugin "my-plugin" mounted
```

| Change                                  | Behavior                                   |
| --------------------------------------- | ------------------------------------------ |
| UI component code (`src/ui/**`)         | Auto-reloads on save                       |
| CSS / Tailwind classes                  | Auto-reloads on save                       |
| Background module (`src/background/**`) | Auto-rebuilds and hot-reloads              |
| `plugin.json`                           | Restart `pnpm dev`                         |
| New dependencies                        | Run `pnpm install` then restart `pnpm dev` |

### Manual Testing (No Dev Server)

If you prefer to test with a production build:

1. Run `pnpm build`
2. Copy the `dist/` contents to Stratos's plugins directory:

| Platform | Path                                                       |
| -------- | ---------------------------------------------------------- |
| macOS    | `~/Library/Application Support/Stratos/plugins/my-plugin/` |
| Windows  | `%APPDATA%\Stratos\plugins\my-plugin\`                     |
| Linux    | `~/.config/Stratos/plugins/my-plugin/`                     |

3. Restart Stratos — it discovers plugins by finding `plugin.json` in each subdirectory.

**Tip:** Symlink your `dist/` directory to skip the copy step:

```bash theme={null}
# macOS
ln -s "$(pwd)/dist" ~/Library/Application\ Support/Stratos/plugins/my-plugin
```

***

## Deploying

### Quick Deploy

The SDK includes a `stratos-deploy` CLI that bundles and uploads your plugin in one step:

```bash theme={null}
pnpm bundle
```

This runs `pnpm build`, zips `dist/` into `bundle.zip`, and uploads it to the Skyvex API. Your `package.json` should have:

```json theme={null}
{
  "scripts": {
    "bundle": "pnpm build && stratos-deploy"
  }
}
```

If you scaffolded your plugin with `create-stratos-plugin`, this script is already included.

### Authentication

Create an API token in your account settings at [skyvexsoftware.com](https://skyvexsoftware.com) — under **Settings → API tokens** — and tick the **`plugin:upload`** scope. Then set it as an environment variable:

```bash theme={null}
SKYVEX_API_TOKEN=your-token pnpm bundle
```

The token must carry the `plugin:upload` scope. Uploads made with a token that lacks it are rejected with `403 Invalid scope(s) provided`. Scopes are fixed when a token is created, so if your token predates this requirement, create a new one with `plugin:upload` ticked and revoke the old one.

The plugin ID and version are read from `plugin.json` automatically. On success, the Skyvex server validates, signs, and publishes your plugin to the CDN.

### Manual Upload

If you don't have an API token or prefer to upload through the website:

```bash theme={null}
pnpm bundle
```

Without `SKYVEX_API_TOKEN`, the command still builds and creates `bundle.zip` — it just skips the upload and tells you where to upload manually at [skyvexsoftware.com](https://skyvexsoftware.com).

### CI/CD

For automated deployments, set `SKYVEX_API_TOKEN` as a secret in your CI environment:

```yaml theme={null}
# GitHub Actions example
- name: Deploy plugin
  run: pnpm bundle
  env:
    SKYVEX_API_TOKEN: ${{ secrets.SKYVEX_API_TOKEN }}
```

#### Full GitHub Actions Workflow

Drop this in your plugin repo at `.github/workflows/deploy.yml` for a complete deploy pipeline that builds the bundle and uploads it to Skyvex on every push to `main`:

```yaml title=".github/workflows/deploy.yml" theme={null}
name: Deploy plugin to Skyvex

# ──────────────────────────────────────────────────────────────
# Builds the plugin and uploads a new version to the Skyvex
# platform. The Skyvex API handles signing, CDN upload, and
# metadata generation from the bundle.
#
# Required GitHub Actions secret:
#   SKYVEX_API_TOKEN  – Passport bearer for the Skyvex API
# ──────────────────────────────────────────────────────────────

on:
  workflow_dispatch:
  push:
    branches: [main]
    paths:
      - "src/**"
      - "assets/**"
      - "plugin.json"
      - "package.json"
      - "pnpm-lock.yaml"
      - "vite.config.ts"
      - "tsconfig.json"
      - ".github/workflows/deploy.yml"

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10

      - name: Read manifest
        id: manifest
        run: |
          VERSION=$(jq -r '.version' plugin.json)
          PLUGIN_ID=$(jq -r '.id' plugin.json)
          echo "version=$VERSION"     >> "$GITHUB_OUTPUT"
          echo "plugin_id=$PLUGIN_ID" >> "$GITHUB_OUTPUT"
          echo "Plugin: $PLUGIN_ID v$VERSION"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile
        timeout-minutes: 5

      - name: Build plugin
        run: pnpm build

      - name: Create bundle.zip
        run: |
          cd dist
          zip -r ../bundle.zip .
          cd ..
          echo "Bundle size: $(du -h bundle.zip | cut -f1)"

      - name: Upload to Skyvex API
        env:
          SKYVEX_API_TOKEN: ${{ secrets.SKYVEX_API_TOKEN }}
          SKYVEX_API_URL: https://skyvexsoftware.com/api/stratos
        run: |
          PLUGIN_ID="${{ steps.manifest.outputs.plugin_id }}"

          RESPONSE=$(curl -s -w "\n%{http_code}" \
            -X POST "${SKYVEX_API_URL}/plugins/${PLUGIN_ID}/versions" \
            -H "Authorization: Bearer ${SKYVEX_API_TOKEN}" \
            -H "Accept: application/json" \
            -F "file=@bundle.zip")

          HTTP_CODE=$(echo "$RESPONSE" | tail -1)
          BODY=$(echo "$RESPONSE" | sed '$d')

          if [ "$HTTP_CODE" -ne 201 ]; then
            echo "Upload failed (HTTP $HTTP_CODE):"
            echo "$BODY"
            exit 1
          fi

          echo "Plugin uploaded and signing initiated:"
          echo "$BODY" | jq .
```

**Setup steps:**

1. **Add the workflow file** at `.github/workflows/deploy.yml` in your plugin repo.
2. **Generate an API token** at [skyvexsoftware.com](https://skyvexsoftware.com) under **Settings → API tokens**, with the **`plugin:upload`** scope ticked.
3. **Add the secret** in your GitHub repo: Settings → Secrets and variables → Actions → New repository secret. Name it `SKYVEX_API_TOKEN` and paste the token.
4. **Bump `plugin.json` version** before each push — the API rejects duplicate versions.
5. **Push to `main`** (or trigger manually from the Actions tab). The workflow builds the bundle and uploads it; the Skyvex API handles signing and CDN publishing.

The workflow only runs when files that affect the bundle change — edits to `README.md` or unrelated files won't trigger a deploy. Use `workflow_dispatch` to force a run from the Actions tab.

### What Happens After Upload

1. The Skyvex API validates your `plugin.json` manifest
2. An `.integrity` file is generated with SHA256 hashes for every file in your bundle
3. The bundle is signed with Ed25519 and republished to the CDN
4. `latest.json` is updated so Stratos clients detect the new version
5. Users receive the update automatically

### Troubleshooting

| Error                       | Cause                                      | Fix                                                                                         |
| --------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------- |
| "No built plugin found"     | `dist/` doesn't exist                      | Run `pnpm build` first, or use `pnpm bundle` which builds automatically                     |
| "Version X already exists"  | This version is already published          | Bump the version in `plugin.json`                                                           |
| "Authentication failed"     | Invalid or expired token                   | Generate a new token at skyvexsoftware.com                                                  |
| "Invalid scope(s) provided" | Token is missing the `plugin:upload` scope | Create a new token with `plugin:upload` ticked — scopes can't be added to an existing token |
| "Permission denied"         | Token doesn't own this plugin              | Check you're using the correct account's token                                              |
| "Build output incomplete"   | `dist/ui/index.js` is missing              | Check your Vite config and build output                                                     |

### Airline Plugin Sync

When a plugin has `"type": "airline"`, the VA platform manages distribution. Stratos automatically syncs the plugin set on authentication:

1. Pilot logs in to their VA
2. Shell fetches the airline's public plugin list (every plugin the VA has installed for everyone, plus core plugins)
3. After relay-token authentication, the shell fetches the pilot-scoped list — this returns the airline-wide plugins plus any extras the VA admin has assigned to that specific pilot
4. Missing plugins are downloaded and installed automatically; removed plugins are cleaned up
5. Updates are applied automatically when new versions are published

VA admins choose, per plugin, whether to install it for **all pilots** or only for **selected pilots** from the airline management panel. Pilot-specific assignments only appear in the post-login (relay-authenticated) plugin list, never in the public airline list.

For `"user"` type plugins, pilots install them manually.
