Metadata-Version: 2.3
Name: joulectl
Version: 0.3.2
Summary: Joule CLI tool to manage Joule transports, streams and use cases
License: MIT
Keywords: joule,streaming
Author: Lyndon Adams
Author-email: lyndon.adams@fractalworks.io
Requires-Python: >=3.10
Classifier: License :: OSI Approved :: MIT License
Requires-Dist: certifi (>=2026.10.19)
Requires-Dist: click (>=8.1.8,<9.0.0)
Requires-Dist: cryptography (>=48.0.1)
Requires-Dist: pyyaml (>=6.0,<7.0)
Requires-Dist: requests (>=2.33.0,<3.0.0)
Requires-Dist: tabulate (>=0.9.0,<1.0.0)
Requires-Dist: urllib3 (>=2.6.3,<3.0.0)
Project-URL: Documentation, https://docs.fractalworks.io/joule
Project-URL: Homepage, https://getjoule.io/
Description-Content-Type: text/markdown

# joulectl

**The command-line tool for [Joule](https://getjoule.io), the low-code real-time stream processing engine.**

![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)
![License: MIT](https://img.shields.io/badge/license-MIT-green)

Use `joulectl` to run a complete Joule stack on your own computer, sign in with an API key, and deploy and manage your transports, streams and use cases from the terminal. Joule connects to a wide range of data sources (Kafka, MQTT, RabbitMQ, InfluxDB, MinIO S3 and more). It adds machine learning inference, custom analytics and event windows, all described in simple YAML specifications.

## Contents

| | Section | What you will find |
|---|---|---|
| 1 | [Overview](#overview) | What joulectl does and how the parts fit together |
| 2 | [Getting started](#getting-started) | Five steps from nothing to a running use case |
| 3 | [Command reference](#command-reference) | Every command, argument and option |
| 4 | [Configuration](#configuration) | Global options, environment variables, the config file, TLS |
| 5 | [Access control](#access-control) | API keys, roles, scopes and the administrator procedures |
| 6 | [Licence](#licence) | Tiers, installing, checking and renewing a licence |
| 7 | [Running Joule locally](#running-joule-locally) | The local stack in depth |
| 8 | [Upgrading](#upgrading) | Changes that can stop your system, and the upgrade steps |
| 9 | [Troubleshooting](#troubleshooting) | Symptoms, error messages and fixes |
| 10 | [Known limitations](#known-limitations) | What joulectl and the platform do not do yet |
| 11 | [Resources](#resources) | Documentation, community and support |

---

## Overview

### What you can do

| Goal | How |
|---|---|
| **Run Joule on your computer** | `joulectl local` starts Guardian, the database, Joule Studio and a Joule daemon (and optionally Kafka) on Docker Compose or minikube. |
| **Build use cases visually** | Open Joule Studio, describe a use case in plain English and deploy it. |
| **Deploy and manage** | Deploy, redeploy, undeploy, pause and resume transports, streams and use cases on any Joule environment. |
| **Inspect and monitor** | List what is deployed, save specifications, and watch live counters for pods, use cases and transports. |
| **Control access** | Sign in with an API key. Give each person or script a role or an exact list of scopes. |
| **Manage your licence** | Install, check and renew your Joule licence token. |
| **Bring your own code** | Load custom jars (Kafka deserializers, processors, UDFs) into the local daemon. |
| **Automate** | `-o json` on every read command, and a non-zero exit code on every failure, so it is safe in CI: `joulectl deploy usecase spec.yaml --pod local_pod && ...` |

### How the parts fit together

```text
                         +-------------------+
   you / scripts / CI -->|     joulectl      |   REST + X-API-Key
                         +---------+---------+
                                   |
        +--------------------------v--------------------------+
        |                     Guardian                        |
        |   control plane and REST API   (port 9080)          |
        |   checks the API key, the scope and the licence     |
        +------+------------------+---------------------+-----+
               |                  |                     |
        +------v------+    +------v-------+     +-------v--------+
        |  Joule DB   |    | Joule daemon |     | Joule Studio   |
        |  (Postgres) |    | runs your    |     | web console    |
        |             |    | use cases    |     | (port 3000)    |
        +-------------+    +------+-------+     +----------------+
                                  |
                      sources and sinks: Kafka, MQTT, files, ...
```

`joulectl` is a thin client. Each command makes one call to Guardian's REST API (`/joule/management/*`), the same API that Joule Studio uses. The one exception is `joulectl local`, which does not call Guardian. It runs `docker`, `kubectl` and `minikube` on your computer to start and stop the stack.

---

## Getting started

Five steps take you from nothing to your first running use case.

| Step | You do |
|---|---|
| [1. Install](#1-install) | Install joulectl and check your computer |
| [2. Add your licence](#2-add-your-licence) | Install your licence token |
| [3. Start Joule](#3-start-joule) | Make a project and start the stack |
| [4. Check it is running](#4-check-it-is-running) | Examine the stack and your sign-in |
| [5. Build a use case](#5-open-joule-studio-and-build-a-use-case) | Sign in to Joule Studio and deploy |

### 1. Install

You need **Python 3.10 or newer**. To run Joule on your computer you also need **Docker** (with Docker Compose v2) or **minikube** with **kubectl**.

```bash
pip install joulectl
joulectl --version
```

Check that your computer is ready to run the stack. Add `--backend minikube` if you prefer minikube.

```bash
joulectl local doctor
```

### 2. Add your licence

Joule starts daemons and deploys use cases only while it holds a valid licence token. Fractalworks issues the tokens. Request one at <https://www.fractalworks.io>. Save the token in a safe place, then install it. The examples use `~/Downloads/joule-licence.jwt`.

```bash
joulectl init --token @~/Downloads/joule-licence.jwt
```

`init` checks the token, shows what it allows, and installs it for your user in `~/.config/joule/license.jwt` (readable only by you). A successful install looks like this:

```text
Tier       poc
Licensee   my-company
Expires    2026-11-16T19:04:48Z  (29 days left)
Daemons    1
Use cases  5

Installed for this user: /Users/you/.config/joule/license.jwt

POC licence: one Joule daemon, up to 5 use cases.
```

You can also paste the token (`--token eyJhbGci...`) or pipe it in (`pbpaste | joulectl init --token -`). See [Licence](#licence) for the tiers and for renewing.

### 3. Start Joule

Make a folder for your Joule project and run everything from it. `joulectl local` keeps its files in a `.joule/` folder inside the directory where you run `init`. That folder holds your licence token and generated secrets, so use a dedicated folder, not a shared or source-controlled one.

```bash
mkdir -p ~/joule-sandbox && cd ~/joule-sandbox

joulectl local init          # make the .joule/ project (add --backend minikube for minikube)
joulectl local up --wait     # start the stack and wait until Guardian is healthy
```

The first start pulls the Joule images, so it can take a few minutes. A progress bar shows the work.

`up` does three things for you:

1. It starts every component of the stack.
2. It points `joulectl` at the new stack, so every other command works against it at once.
3. With `--wait`, it signs you in. It uses the one-time secret of the project to make an **administrator API key** that lasts 30 days, and it stores the key for you.

### 4. Check it is running

```bash
joulectl local status        # every component is running (healthy)
joulectl whoami              # who you are, the scopes of your key, and when it ends
joulectl ls pods             # the local Joule daemon registers as the pod "local_pod"
joulectl license status      # Guardian accepted your licence
```

Guardian's health endpoint is <http://127.0.0.1:9080/health>.

### 5. Open Joule Studio and build a use case

Joule Studio is the web console of Joule. It starts with the stack. `local status` calls it `joule-ui`.

1. **Sign in.** Open <http://127.0.0.1:3000> (or the port you gave `--ui-port`). Studio asks for an API key. Print the key of your local stack and paste it into the dialog:

   ```bash
   joulectl local key | pbcopy      # macOS. On other systems, run "joulectl local key" and copy the output.
   ```

   If it says `No API key is stored for <host:port>`, run `joulectl local login` first. That makes an administrator key and stores it. `local up --wait` does the same step.

   Studio keeps the key for one browser tab. Closing the tab signs you out. Studio asks again when the key ends or an administrator revokes it.

   > [!WARNING]
   > Treat the key like a password. Do not put it in a chat message, an email or a ticket.

2. **Choose the pod.** Where Studio asks for a pod, choose **`local_pod`**, the local Joule daemon.
3. **Make sure your data connectors exist.** A use case reads from *sources* and writes to *sinks*. Look under **Connectors → Sources** and **Connectors → Sinks**. If a connector is missing, add it there, or deploy it from the terminal: `joulectl deploy transport my_source.yaml --pod local_pod`. If a connector uses your own Java classes (a Kafka deserializer, for example), add the jar first: `joulectl local userlib add YOUR.jar` (see [Custom jars](#custom-jars)). To use Kafka, start the stack with a broker: `joulectl local up --with-kafka`.
4. **Describe your use case.** Open **Workspace → Use Case Assistant** and write what you want in plain English. Name the sources and sinks. For example: *"Subscribe to nasdaq quotes, filter for CVCO symbols, and publish to the cvco analytics sink."* Select **Generate**. The assistant picks the sources and sinks that your description names (you can tick or untick rows yourself) and builds the stream. If it cannot decide something, such as which field is the price, it asks you.
5. **Review.** Check the use case name, the pod, the flow (sources, your stream, sinks) and the processing period (*Valid From* and *Valid To*). The assistant fills in the period if you say when, for example "for the next 30 days". **Inspect** shows the exact definitions that will be deployed. To change the stream, choose **Edit in Stream Builder**, then **Return to Use Case Assistant**.
6. **Deploy.** Select **Deploy use case**. Joule deploys the stream first, then the use case. Choose **Go to Monitor** to watch it run.

> [!IMPORTANT]
> **The assistant needs an AI model.** If Guardian has no AI model, Studio shows an amber banner and uses a basic demo generator. You **cannot deploy** the results of the demo generator. Set a model under **Settings → AI assistant**, then generate again. Only your description and the *field names* of the chosen sources go to the model, never event values.

Prefer to write the specification yourself? Use the **Stream Builder** (under **Streams**) in Studio, or deploy a YAML file from the terminal:

```bash
joulectl deploy usecase my_usecase.yaml --pod local_pod
```

Either way, follow your use case from the terminal:

```bash
joulectl ls usecases                                   # the use case is listed
joulectl status usecase my_usecase                     # live counters (add --events for sample events)
joulectl process pause my_usecase --pod local_pod      # pause processing (resume with "process resume")
joulectl inspect usecase my_usecase --pod local_pod    # save its specification to a file
```

### Stop and start again

```bash
joulectl local down          # stop the stack; your data stays and your previous host setting returns
joulectl local up --wait     # start it again with the same data
```

To remove everything, including the `.joule/` folder and all data: `joulectl local delete --purge`.

---

## Command reference

Every command has its own `--help` with examples. Names in capitals are arguments. Options in square brackets are optional.

> [!TIP]
> The **Scope** column shows the API key scope that Guardian needs for the command. The scope names are explained in [Scopes](#scopes). A role (`viewer`, `operator`, `admin`) holds the scopes it needs. Commands that do not call Guardian need no scope.

### Deploy and manage

| Command | What it does | Scope |
|---|---|---|
| `deploy usecase FILE [--pod POD]` | Deploy a use case from a YAML or JSON specification file. | `usecases:write` |
| `deploy stream FILE [--pod POD]` | Deploy (register) a stream. | `streams:write` |
| `deploy transport FILE [--pod POD]` | Deploy (register) a transport, which is a source or a sink. | `transports:write` |
| `redeploy usecase NAME FILE [--pod POD]` | Replace a deployed use case with an updated specification in one atomic step, so the use case is never missing in between. | `usecases:write` |
| `undeploy usecase NAME [--pod POD]` | Remove a use case. | `usecases:write` |
| `undeploy stream NAME [--pod POD]` | Remove a stream. | `streams:write` |
| `undeploy transport NAME TYPE [--pod POD]` | Remove a transport. `TYPE` is `SOURCE` or `SINK`. | `transports:write` |
| `process pause NAME [--pod POD]` | Pause the processing of a use case. | `usecases:write` |
| `process resume NAME [--pod POD]` | Resume the processing of a use case. | `usecases:write` |

### List, inspect and monitor

| Command | What it does | Scope |
|---|---|---|
| `ls pods` | List the running pods and the number of Joule processes in each. | `pods:read` |
| `ls members POD` | List the running members of a pod. | `pods:read` |
| `ls usecases` | List the deployed use cases. | `usecases:read` |
| `ls streams [--pod POD]` | List the registered streams. | `streams:read` |
| `ls transports [--pod POD] [--type SOURCE\|SINK]` | List the deployed transports. | `transports:read` |
| `inspect usecase NAME [--pod POD] [--filename FILE]` | Save a deployed use case specification to a file. | `usecases:read` |
| `inspect stream NAME [--pod POD] [--filename FILE]` | Save a deployed stream specification to a file. | `streams:read` |
| `inspect transport NAME TYPE [--pod POD] [--filename FILE]` | Save a deployed transport specification to a file. | `transports:read` |
| `status pod NAME` | Live counters for a pod. | `pods:read` |
| `status usecase NAME [--pod POD] [--events]` | Live counters for each process of a use case. `--events` adds samples of the last events emitted. | `usecases:read` |
| `status transport NAME TYPE [--pod POD]` | Live statistics for each process of a deployed transport. | `transports:read` |

`ls` shows what is deployed. `status` shows what is happening now.

### Sign in and manage API keys

| Command | What it does | Scope |
|---|---|---|
| `login [--key-file FILE] [--no-verify]` | Store an API key for the Guardian you talk to. Guardian checks the key first. Without `--key-file`, joulectl reads the key from a pipe or from a prompt that does not show it. `--no-verify` stores the key without asking Guardian. | none |
| `logout` | Remove the stored key for this Guardian. This only makes joulectl forget the key. The key is **not revoked** and Guardian cannot show it again, so keep a copy if you want to sign in later. | none |
| `whoami` | Show the owner, the scopes and the end time of your key, and where joulectl got the key. | any valid key |
| `apikey create (--role ROLE \| --scope SCOPE...) --ttl TTL [--owner OWNER] [--description TEXT] [--save]` | Make a key. Guardian shows the key **once**. `--save` also stores it, as `login` does. | `apikeys:self` or `apikeys:admin` |
| `apikey ls [--owner OWNER]` | List keys: your own, or all keys with `apikeys:admin`. It never shows a key itself. | `apikeys:self` or `apikeys:admin` |
| `apikey revoke ID [-y]` | Stop a key at once, and ask first (`-y` / `--yes` skips the question). `ID` is the id or the prefix from `apikey ls`. The start of either is enough if it is unique. | `apikeys:self` or `apikeys:admin` |

See [Access control](#access-control) for the full explanation.

### Licence

| Command | What it does | Scope |
|---|---|---|
| `init --token TOKEN [--no-apply]` | Check a licence token, install it for your user, and send it to Guardian if one is running. `TOKEN` is the token, `@path/to/file`, or `-` for stdin. | `license:admin` to replace the licence |
| `license status` | Show the licence that Guardian uses. If Guardian does not run, it shows the installed token. | `license:read` |
| `license apply TOKEN` | Replace the licence of a running Guardian, with no restart. | `license:admin` |

### Settings

| Command | What it does |
|---|---|
| `config create` | Make the configuration file `~/.config/joule/config.json`. |
| `config show` | Show the settings. The API key is masked. |
| `config update [OPTIONS]` | Change one or more settings. You must give at least one option. |

Options of `config update`:

| Option | Meaning |
|---|---|
| `--host HOST` | Guardian address: `host:port`, `http://host:port` or `https://host:port`. |
| `--timeout SECONDS` | Request timeout. |
| `--api-key-file FILE`, `--api-key KEY` | A default API key that joulectl sends to **every** host. Prefer `joulectl login`, which keeps one key for each Guardian. |
| `--ca-bundle FILE` | PEM file with the CA that signed an `https://` Guardian. |
| `--verify-images` / `--no-verify-images` | Require a valid [cosign](https://docs.sigstore.dev/cosign/) signature on every image that `local up` runs. |
| `--cosign-key FILE` | The cosign public key that signed the images. |
| `--cosign-identity REGEXP`, `--cosign-issuer URL` | Keyless signing: the identity and the OIDC issuer that vouch for the images. |

### The local stack

| Command | What it does |
|---|---|
| `local doctor [--backend docker\|minikube]` | Check the tools, the licence, the sign-in and your custom jars. It exits with an error if a required tool is missing. |
| `local init [OPTIONS]` | Make the `.joule/` project in the current folder. |
| `local up [OPTIONS]` | Start the stack and point joulectl at it. |
| `local status` | Show the state of each component and the endpoints. |
| `local logs [COMPONENT] [-f] [--tail N]` | Show logs. `COMPONENT` is `guardian`, `daemon`, `ui`, `db` or `kafka`. |
| `local exec COMPONENT [COMMAND...]` | Run a command in a component, or open a shell. |
| `local psql [ARGS...]` | Open `psql` on the Joule metadata database. |
| `local userlib add FILE.jar... \| ls \| rm NAME...` | Manage the custom jars of the daemon. |
| `local login [--recover] [-y]` | Make a new administrator key for the local stack and store it. `--recover` is for a lost key: it revokes every active key first (see [Recover when no administrator key works](#recover-when-no-administrator-key-works)). |
| `local key` | Print the stored local key, to sign in to Joule Studio. |
| `local connect [--print]` / `local disconnect` | Point joulectl at the local stack, or restore your previous host. |
| `local down [--volumes] [--keep-connection]` | Stop the stack. Your data stays. |
| `local reset [OPTIONS]` | Delete all local data, pull new images and start again. |
| `local delete [--purge] [-y]` | Remove the containers, volumes and network. |

Details, options and examples are in [Running Joule locally](#running-joule-locally).

---

## Configuration

### Global options

Every command accepts these options before the command name, for example `joulectl -o json ls usecases`. A setting comes from the first source that has it: **command-line flag, then environment variable, then config file, then built-in default**.

| Flag | Environment variable | Default | Purpose |
|---|---|---|---|
| `--host` | `JOULE_HOST` | `127.0.0.1:9080` | Guardian address: `host:port`, `http://host:port` or `https://host:port` |
| `--api-key-file` | `JOULE_API_KEY_FILE` | none | A file that holds the API key to send as `X-API-Key` |
| `--api-key` | `JOULE_API_KEY` | none | The API key itself. It is visible in the process list and the shell history, so prefer `--api-key-file` |
| `--ca-bundle` | `JOULE_CA_BUNDLE` | system CAs | PEM file to verify an `https://` Guardian that a private CA signed |
| `--insecure` | `JOULE_INSECURE` | off | Skip the TLS certificate check. Use only as a last resort |
| `--timeout` | `JOULE_TIMEOUT` | `30` | Request timeout in seconds |
| `-o, --output` | `JOULE_OUTPUT` | `table` | `table` or `json` |
| `-v, --verbose` | none | off | `-v` traces each request. `-vv` adds full debug logging. The output goes to stderr, so `-o json` output stays clean |
| `--version` | none | none | Show the installed version |

Example: `joulectl -o json ls usecases | jq '.[].name'`

### Which API key does joulectl send?

joulectl uses the first of these:

1. The key from `--api-key`, `--api-key-file` or `JOULE_API_KEY`.
2. The key that `joulectl login` stored for **this** Guardian.
3. The older single `api_key` setting from `config update`.

joulectl never sends a stored key to a different Guardian. Keys are stored by address (`scheme://host:port`) in the config file, which only you can read.

### The config file

`~/.config/joule/config.json` holds the host, the timeout, the CA bundle, your stored keys and your image-signature policy. joulectl writes it for your user only (permissions `0600`, in a `0700` folder). `config show` never prints a key in full.

### Connecting to a remote Guardian

After `joulectl local up`, joulectl already points at your local stack. To manage a different Guardian, set its host and sign in:

```bash
joulectl config update --host https://joule.example.com
joulectl login                  # paste the key that your administrator gave you
joulectl whoami
```

### Use HTTPS on every network that you do not trust

An API key is in each request. Guardian serves plain HTTP, so put a TLS-terminating proxy or an Ingress in front of it and use an `https://` host:

```bash
joulectl --host https://guardian.example:9443 --api-key-file ~/.config/joule/api.key ls pods

# or remember the settings
joulectl config update --host https://guardian.example:9443 --ca-bundle ~/certs/our-ca.pem
```

joulectl protects your credentials on the wire:

- It never follows a redirect, because a redirect could carry your key to another host.
- It warns once for each host before it sends a key or a licence token over plain HTTP to an address that is not on your computer.
- It shows a clear `TLS error ...` message, with the way out, when it cannot check a certificate.
- It makes escape sequences in text from the server visible (`\x1b`), so a hostile server cannot rewrite your screen.

---

## Access control

Guardian does not have user accounts or passwords. **A person or a script signs in with an API key.** Guardian checks the key and its scopes on every request.

> [!NOTE]
> **An API key is not a licence.** The API key (`jk_live_...`) says who you are. It signs you in to Guardian, Joule Studio and joulectl. The [licence](#licence) is a different token (a JWT) that says what your installation may run. You install it with `joulectl init --token`. If you revoke an API key, the licence does not change. If your licence ends, your API keys still sign you in, but Guardian refuses changes with a licence error (`LICENSE_EXPIRED`).

### How keys work

| Property | Meaning |
|---|---|
| **Format** | `jk_live_` followed by 43 characters. |
| **Owner** | A name that an administrator types when the key is made. Use an email address for a person, or a service name for a script. Guardian does not check that the name is a real person. |
| **Scopes** | The list of things the key may do. See [Scopes](#scopes). |
| **End time** | Every key ends. The longest life is 90 days, unless the administrator of Guardian changed it. |
| **Shown once** | Guardian shows the key only when it makes the key and keeps only a hash. If a user loses a key, make a new key. `apikey ls` never shows a key. joulectl keeps its own copy of a key that it stored (`login`, or `local up --wait`) in `~/.config/joule/config.json`, which only you can read. On the local stack, `joulectl local key` prints that copy. `joulectl logout` removes that copy. The key then still works (for example in Studio), but joulectl cannot print it again. |

Two limits protect against privilege growth. A key can give only the scopes that it holds itself. A key cannot make a key that ends after itself. A key with the scope `apikeys:admin` is free of both limits.

### Roles

A role is a set of scopes. Choose a role with `--role` when you make a key. Guardian stores the scopes, not the role name.

| Role | The user can do this |
|---|---|
| `viewer` | Read all lists and details. Use the Use Case Assistant. |
| `operator` | Do what a viewer does. Also deploy, undeploy, pause and resume use cases. Register streams and transports. Change the library. Deploy context data. Make and revoke keys for the same owner. |
| `admin` | Do everything. This includes deploying code, changing the AI model settings, deleting the audit log, replacing the licence, and making keys for other owners. |

An operator cannot deploy or removeip code (scripts). An operator can deploy context data but cannot remove it. The roles can change in a later release. If you need a fixed set, make keys with scopes.

### Scopes

A scope has the form `feature:level`. A higher level includes the lower levels. For example, `usecases:write` includes `usecases:read`. Use scopes when no role fits. Add one `--scope` option for each scope.

<details>
<summary><strong>Show the full list of scopes</strong></summary>

| Scope | What it allows |
|---|---|
| `pods:read` | List pods, members and pod status. |
| `usecases:read`, `usecases:write` | Read: list and inspect use cases. Write: deploy, redeploy, undeploy, pause and resume. |
| `streams:read`, `streams:write` | Read: list and inspect streams. Write: register and unregister. |
| `transports:read`, `transports:write` | Read: list, inspect and measure transports. Write: register and unregister. |
| `library:read`, `library:write` | Read: list the library. Write: add, update and delete items. Deploy ML models. |
| `reconciliation:read`, `reconciliation:write` | Read: the drift report. Write: start a sweep. |
| `dsl:read` | Read the DSL schema and the component catalogue. |
| `artefacts:read` | List scripts and read their source. |
| `artefacts:write` | Deploy, change and remove scripts. The daemons run this code. Also remove context data files. |
| `contextdata:read`, `contextdata:write` | Read: preview context data. Write: deploy context data. |
| `intent:use` | Use the Use Case Assistant. |
| `intent:admin` | Read and change the AI model settings. These hold a third-party key. |
| `audit:admin` | Read and change the audit settings. Delete the log. |
| `license:read`, `license:admin` | Read the licence. Admin: replace the licence. |
| `apikeys:self`, `apikeys:admin` | Make, list and revoke keys of the same owner. Admin: do this for all owners. |

</details>

### Sign in to Joule Studio

Studio asks for a key when Guardian needs one, and again when the key ends or is revoked. Studio keeps the key for one browser tab only. **Settings → API keys** shows your name, the end time of your key and your scopes. A user with `apikeys:self` can also make, list and revoke keys there. Studio hides the buttons that the scopes of the key do not allow. Guardian refuses the request in all cases.

### Procedures

#### Make the first administrator key

On the local stack, `joulectl local up --wait` does this for you. For another Guardian, an administrator who set the bootstrap secret (see [Turn on authentication](#turn-on-authentication-on-your-own-guardian)) does the following:

1. Make the key. The bootstrap secret is the key for this one command:

   ```bash
   joulectl --host <host:port> --api-key-file bootstrap.txt apikey create --role admin --ttl 30d --owner you@example.com
   ```

2. Copy the key that the command shows. The command shows it once.
3. Store the key on your computer: `joulectl --host <host:port> login`
4. Make sure that you are signed in: `joulectl --host <host:port> whoami`
5. Delete the bootstrap file: `rm bootstrap.txt`

Guardian accepts the bootstrap secret only while no administrator key is active. After step 1, Guardian refuses the secret.

#### Add a user

An administrator does these steps.

```bash
joulectl apikey create --role operator --ttl 30d --owner vera@example.com --description "Vera, laptop"
```

Send the key to the user in a private way. Do not send it in a chat or an email. The user then runs `joulectl login`, or pastes the key into the sign-in dialog of Joule Studio. For a user who needs a different set of scopes, use `--scope` instead of `--role`:

```bash
joulectl apikey create --scope usecases:read --scope pods:read --ttl 4h --owner vera@example.com
```

#### Add a service account for a script or CI job

Give the account only the scopes that the job needs.

```bash
joulectl -o json apikey create --scope usecases:read --scope usecases:write \
    --ttl 12w --owner ci-pipeline --description "nightly deploy" > ci-key.json
chmod 600 ci-key.json
```

Put the value of `key` from `ci-key.json` in the secret store of your CI system, then delete the file (`rm ci-key.json`). In the job, set the environment variable `JOULE_API_KEY`. Do not put the key on a command line.

#### Remove the access of a user

```bash
joulectl apikey ls --owner vera@example.com       # find the ids
joulectl apikey revoke <id>                       # do this for each key
```

The key stops at once on the Guardian that you use. On another Guardian it stops in 30 seconds or less. Joule Studio shows the sign-in dialog at the next request.

#### Revoke a key of your own

You do not need to revoke a key that is just in use. A key ends by itself at its end time, and closing the Studio tab signs you out. Revoke a key of your own in these cases:

| Situation | Why |
|---|---|
| The key was shown where others can read it (a chat, a screenshot, a ticket, a CI log, a shell history) | A revoke stops the key at once. Waiting for the end time leaves it usable by anyone who saw it. |
| You made a new key to replace it | The old key stays valid until its end time. Revoke it to leave only one. |
| A laptop, a script or a CI job that held the key is retired | Its key should stop with it. |
| `joulectl apikey ls` shows keys that you no longer use | Fewer active keys means less to look after. |

In Studio, open **Settings → API keys** and revoke the key there (this needs the scope `apikeys:self`). With joulectl, run `joulectl apikey ls`, then `joulectl apikey revoke <id>`.

> [!WARNING]
> Make the new key **before** you revoke the only administrator key. On the local stack, `joulectl local login` makes a new administrator key only while no other administrator key is active. If another one is active, Guardian refuses the bootstrap secret. In that case, run `joulectl local login --recover` (see below). It does not delete your data.

#### Renew a key

| Situation | What to do |
|---|---|
| A user whose key ends soon | Ask an administrator for a new key. A user cannot renew a key beyond the end of the key that makes it. |
| An administrator whose key ends soon | `joulectl apikey create --role admin --ttl 30d`, then `joulectl login`, then `joulectl apikey revoke` for the old key. |
| The local stack | `joulectl local login` (or `joulectl local login --recover` if you lost the key) |

Use `joulectl whoami` to see when your key ends. Joule Studio shows the time in the sidebar.

#### Recover when no administrator key works

Do this when all administrator keys have ended, or an administrator lost the only key.

- **Local stack:** run `joulectl local login`. If a key is still active, Guardian refuses the bootstrap secret. This happens after `joulectl logout` (joulectl forgets the key, but Guardian still counts it as active) and when somebody else holds the administrator key. Then do one of these:
  - Sign in with a copy of the key: `joulectl login`. Studio keeps its key in the browser tab for as long as the tab is open.
  - Run `joulectl local login --recover`. It asks you to confirm (`-y` skips the question), revokes **every** active key in the database of the local stack, and then makes and stores a new administrator key from the bootstrap secret, and prints that key. Guardian remembers a revoked key for up to 30 seconds, so the command can wait up to a minute before the new key is made. Your data stays. Anything that uses an old key, such as Studio, must sign in again with `joulectl local key`.

  The bootstrap secret is not an API key. `joulectl login` refuses it.
- **Another Guardian:** set the bootstrap secret again on Guardian (if you removed it), restart Guardian, and make a new administrator key as above. Guardian accepts the secret only while no administrator key is active. If the lost key has not ended, ask the administrator of your database to revoke it.

#### Find out who is signed in

| Question | Command |
|---|---|
| Who am I? When does my key end? | `joulectl whoami` |
| Which keys exist? | `joulectl apikey ls` |
| Is the local stack set up correctly? | `joulectl local doctor` |

### Turn on authentication on your own Guardian

Do this when you run Guardian yourself and joulectl did not start it. Set these variables in the environment of the Guardian process, then restart Guardian. A value that is not correct stops Guardian from starting.

| Setting | Meaning | Default |
|---|---|---|
| `JOULE_BOOTSTRAP_KEY` | A one-time secret of 32 or more characters. It signs in the first administrator. | not set |
| `JOULE_BOOTSTRAP_KEY_FILE` | A file that holds that secret. | not set |
| `GUARDIAN_AUTH` | `off`, `warn` or `enforce`. In `warn`, Guardian writes each refusal to its log and allows the request. Use `warn` while you find old clients. | `enforce` when a bootstrap secret is set or any key exists, otherwise `off` |
| `GUARDIAN_API_KEY_MAX_TTL_DAYS` | The longest life of a key, in days (1 to 3650). | `90` |
| `GUARDIAN_AUTH_CACHE_SECONDS` | How long Guardian remembers a valid key (1 to 300). A revoke on another Guardian takes this time. | `30` |
| `GUARDIAN_API_KEY_ENV` | The label in a new key: `jk_<label>_...` | `live` |

> [!NOTE]
> You can remove the bootstrap secret after the first administrator key exists. Authentication does not go off. The secret has the scope `apikeys:admin` and no other scope.

To harden a production deployment (HTTPS on Guardian, encrypted connections between Guardian and the daemons, JMX and debugger settings, network policies), see the Joule documentation at <https://docs.fractalworks.io/joule>.

---

## Licence

| Tier | Lasts | Daemons | Use cases |
|---|---|---|---|
| `poc` | 30 days | 1 | unlimited, or capped by the token |
| `master` | 6 months | any number | unlimited, or capped |
| `enterprise` | as agreed (1 year by default) | any number, unless capped | unlimited, or capped |

| Command | What it does |
|---|---|
| `joulectl init --token TOKEN` | Check the token, install it for your user, and send it to Guardian if one is running. `TOKEN` is the token, `@path/to/file`, or `-` for stdin. `--no-apply` installs it without contacting Guardian. |
| `joulectl license status` | Show the licence that Guardian uses: state, tier, expiry, and the daemons and use cases used out of those allowed. If Guardian does not run, it shows the installed token. Supports `-o json`. |
| `joulectl license apply TOKEN` | Replace the licence of a running Guardian without a restart. |

You can install the token before or after `local init`. `local up` copies the installed token into your project each time it starts the stack, and Guardian remembers it, so a restart does not need it again. `joulectl local init --token @file` installs the token in the same step as making the project. `joulectl local doctor` warns when no token is installed. To start Guardian without a licence (it then starts locked), use `local up --no-license-check`.

> [!NOTE]
> When Guardian has authentication, `license apply` and `init --token` need a key with the scope `license:admin`. If Guardian refuses the token for this reason, `init --token` still keeps a valid token on your computer.

### Renew or replace a licence

| Situation | What to run |
|---|---|
| The stack runs. The licence is still valid. | `joulectl license apply @renewed-licence.jwt`. It takes effect at once. |
| The stack runs. The licence has expired. | `joulectl license apply @renewed-licence.jwt`, then `joulectl local up` to restart the daemon that Guardian stopped. |
| The stack does not run. | `joulectl init --token @renewed-licence.jwt`, then `joulectl local up`. |

### Licence problems

| You see | What it means and what to do |
|---|---|
| `No Joule licence installed.` | Run `joulectl init --token ...` first. |
| `This licence token expired on ...` | Request a new token. |
| `This licence token was signed with a key this version of joulectl doesn't recognise.` | Upgrade joulectl (`pip install --upgrade joulectl`). If that does not fix it, request a new token. |
| `...signed with the key '...', which was retired` | Request a re-issued token and install it with `joulectl init --token`. |
| Changes fail with `403` and code `LICENSE_INVALID` or `LICENSE_EXPIRED` | Guardian has no valid licence. Reads still work, changes do not. Run `joulectl license apply`. |
| `403` with code `LICENSE_USECASE_LIMIT` | The use-case limit of the token is reached. Undeploy a use case, or use a token with a higher limit. Redeploying an existing use case does not count. |
| The daemon stops and `joulectl local logs daemon` shows a licence message (exit code 78) | The licence has expired, or the single daemon of a POC token is already running. Renew as above. A second daemon needs a master or enterprise token. |

---

## Running Joule locally

`joulectl local` runs a complete Joule stack on your computer. The [Getting started](#getting-started) steps are all that most people need. This section covers the rest.

### What runs

| Component | Name in `status` and `logs` | Where to find it (Docker) |
|---|---|---|
| Guardian: control plane and REST API | `guardian` | <http://127.0.0.1:9080> (health at `/health`) |
| Joule DB: Postgres metadata store | `jouledb` | `127.0.0.1:5432` |
| Joule Studio: web console (the Joule UI) | `joule-ui` | <http://127.0.0.1:3000> |
| Joule daemon: runs your use cases, registers as pod `local_pod` | `joule-daemon` | REST API and Swagger at <http://127.0.0.1:9081/swagger> |
| Redpanda: Kafka-compatible broker and Schema Registry (optional, `--with-kafka`) | `redpanda` | Kafka `127.0.0.1:19092`, Schema Registry <http://127.0.0.1:18081> |

With minikube, Guardian and Studio are port-forwarded to the same addresses. You can change the ports when you run `init` (`--rest-port`, `--postgres-port`, `--ui-port`, `--kafka-port`, `--schema-registry-port`) and leave components out (`--no-ui`, `--no-daemon`). Guardian takes a few seconds to start and the daemon a little longer. Allow up to a minute for a new start to settle, or use `up --wait`.

### Commands in detail

| Command | What it does |
|---|---|
| `local doctor` | Check Docker (or minikube and kubectl), the licence, the sign-in, the image lock and your custom jars. It exits with an error if something required is missing, so you can use it in a script. A missing licence or a stopped minikube cluster is a `WARN`, not a failure. |
| `local init` | Make the `.joule/` project in the current folder. It refuses to overwrite an existing project without `--force`. |
| `local up` | Start the stack and point joulectl at it. |
| `local status` | The state of each component and the endpoints. Supports `-o json`. |
| `local logs [COMPONENT]` | Logs for `guardian`, `daemon`, `ui`, `db` or `kafka` (or all). `-f` / `--follow` follows. `--tail N` limits the lines (default 200). |
| `local exec COMPONENT [COMMAND...]` | Run a command in a component, or open a shell. Everything after the component is the command, flags included. |
| `local psql [ARGS...]` | Open `psql` on the Joule metadata database. You do not need the password. Arguments and `stdin` go to `psql` unchanged. |
| `local userlib add\|ls\|rm` | Manage custom jars for the daemon (see [Custom jars](#custom-jars)). |
| `local login [--recover] [-y]` | Make an administrator key for the local stack and store it. Use it after a plain `up`, when your key has ended (it lasts 30 days), or after a reset. Add `--recover` if you lost the key (for example after `joulectl logout`): it revokes all active keys first, then makes a new one. |
| `local key` | Print the stored key, for Joule Studio. It prints only the key to `stdout`, so you can pipe it. First it asks Guardian if the key still works. If the key has ended or is revoked, the command prints an error and no key. Guardian keeps only a hash of each key, so the command cannot print any other key. Use `joulectl apikey ls` to list the keys that exist. |
| `local connect` / `local disconnect` | Point joulectl at the local endpoint, or restore the previous host. `connect --print` only shows the endpoint. |
| `local down` | Stop the stack. Your data stays. `--volumes` also deletes the data volumes. `--keep-connection` leaves joulectl pointed at the local endpoint. |
| `local reset` | Delete all data, pull the newest images and start again. It asks first; `-y` / `--yes` skips the question. Options: `--no-pull` keeps the cached images, `--wait` waits for Guardian, `--no-connect` leaves your host setting alone. |
| `local delete` | Remove the containers, volumes and network (or the whole `joule` namespace on minikube). `--purge` also removes `.joule/`. It asks first; `-y` / `--yes` skips the question. |

#### Options of `local init`

| Option | Meaning |
|---|---|
| `--backend docker\|minikube` | The default orchestrator for `local up`. Default `docker`. |
| `--image-tag TAG` | The tag of the `fractalworks/joule-*` images. The minimum is `1.3.0`. Default `latest`. |
| `--rest-port N`, `--postgres-port N`, `--ui-port N` | The host ports for Guardian, the database and Studio. |
| `--with-kafka`, `--kafka-port N`, `--schema-registry-port N` | Add a Redpanda broker and set its ports. |
| `--with-ui` / `--no-ui`, `--with-daemon` / `--no-daemon` | Include or leave out Studio and the daemon. Both are included by default. |
| `--bind ADDR` | Docker: the address to publish ports on. Default `127.0.0.1`. |
| `--minikube-profile NAME` | minikube: the profile and kube-context to use. |
| `--from DIR` | Use the Kubernetes manifests of a Joule deployment checkout. |
| `--token TOKEN` | Install a licence token in the same step. |
| `--force` | Overwrite an existing `.joule/`. Your database volume and custom jars stay. |

#### Options of `local up`

| Option | Meaning |
|---|---|
| `--wait`, `--wait-timeout N` | Wait until Guardian's `/health` answers (default 120 seconds), then sign you in. |
| `--pull` | Pull the newest images first. |
| `--load-images` | minikube: replace the images in the cluster with your local Docker builds. |
| `--with-kafka` / `--no-kafka`, `--with-daemon` / `--no-daemon`, `--with-ui` / `--no-ui` | Turn a component on or off for this run. A component that you turn **off** is also stopped if it runs. |
| `--backend`, `--image-tag` | Override what `init` recorded. `--image-tag` rewrites the project files, so it sticks. |
| `--no-connect` | Do not change the host setting of joulectl. |
| `--no-license-check` | Start without a valid licence token. Guardian starts locked. |
| `--trust`, `--trust-images` | Accept a project or an image that differs from what joulectl recorded (see [Security of the local stack](#security-of-the-local-stack)). |

```bash
joulectl local logs guardian -f           # follow Guardian's logs
joulectl local up --with-kafka            # add a Redpanda broker
joulectl local up --no-ui                 # leave Studio out
joulectl local psql -c '\dt'              # look at the metadata database
joulectl local exec guardian              # open a shell in Guardian's container
joulectl -o json local status | jq '.components[] | select(.state != "running")'
```

### Wait for the stack in scripts and CI

`up` returns when the containers start, which is before Guardian has finished starting. In a script, add `--wait`:

```bash
joulectl local up --wait && joulectl deploy usecase my_usecase.yaml --pod local_pod
joulectl local up --wait --wait-timeout 300     # slow computer, or a cold image pull
```

If the container of Guardian exits while you wait (for example a wrong configuration or no database), `--wait` stops at once and points to `joulectl local logs guardian`. On a timeout it also exits with an error but leaves the stack running, so you can look at it or run `joulectl local down`. `reset --wait` works the same way.

### What `init` makes

```text
.joule/
  local.json              state: backend, image tag, ports, enabled components, what runs
  .env                    values that docker compose reads (image tag, ports, secrets)
  docker-compose.yaml     the Docker stack (Studio, daemon and Kafka are compose profiles)
  conf/                   Guardian, daemon and Studio configuration
  kubernetes/             namespace, database, guardian, joule-daemon, joule-ui manifests
  userlibs/               your own jars for the daemon (see Custom jars)
  logs/                   Guardian and daemon logs, and the minikube port-forward log
```

You can edit these files. `up` uses what is on the disk. A bundled copy of the manifests comes with the package, so `pip install` is all that you need. The images are `fractalworks/joule-db`, `joule-guardian`, `joule-daemon` and `joule-ui`. Choose a version with `--image-tag`.

### Use minikube

```bash
joulectl local doctor --backend minikube
joulectl local init --backend minikube
joulectl local up --wait
```

joulectl talks only to the minikube profile that you chose (`--minikube-profile`, default `minikube`), never to the cluster that `kubectl` currently points at. If the REST endpoint does not respond, look at `.joule/logs/port-forward.log` and run `joulectl local status`.

Before it applies the manifests, `up` makes sure that each image is in minikube. It loads an image from your local Docker if it is there, and otherwise pulls it. An image with `imagePullPolicy: Never` that is missing stops the command with a list of the missing images. After you rebuild an image locally, run `joulectl local up --load-images`.

### Custom jars

If a specification names one of your own classes (a Kafka deserializer such as `parser: com.mycompany.QuoteParser`, a custom processor, a UDF), the jar that holds it must be on the classpath of the daemon. Without it, the daemon logs `ClassNotFoundException`, and Guardian reports `Malformed transport specification` or `Failure parsing stream dsl file`. One command copies the jar in and restarts the daemon:

```bash
joulectl local userlib add build/libs/banking-example-1.2.1.jar
joulectl local userlib ls                      # name, size and SHA-256
joulectl deploy transport my_kafka_source.yaml --pod local_pod
joulectl local userlib rm banking-example-1.2.1.jar
```

Jars live in `.joule/userlibs/` and survive `down`, `reset` and `init --force`. joulectl checks each jar before it copies it: a real zip file, not empty, under 512 MiB, with a plain file name that ends in `.jar`. `--no-restart` stages a jar without restarting the daemon.

> [!CAUTION]
> A jar is code that runs inside the daemon, with its network and database access. Add only jars that you trust. For a remote Joule, put the jar in the `userlibs` folder of that daemon and restart the daemon.

### Your data: what each command keeps

| Command | Containers or pods | Database and data volumes | `.joule/` folder | joulectl host setting |
|---|---|---|---|---|
| `down` | removed | **kept** | kept | restored |
| `down --volumes` | removed | **deleted** | kept | restored |
| `reset` | recreated | **deleted**, then recreated empty | kept | pointed at local |
| `delete` | removed | **deleted** | kept (`--purge` removes it) | restored |

### Update the Joule images

Upgrade the tool with `pip install --upgrade joulectl`. Refresh the images and keep your data with `joulectl local up --pull`.

The database schema is made only in an empty database. When a Joule release adds or changes tables, run `joulectl local reset` to rebuild the database on the newest images. This deletes everything stored in the stack: deployed transports, streams and use cases (and, on minikube, scripts, context data and models). Your `.joule/` files stay. After a `reset`, sign in again with `joulectl local up --wait` or `joulectl local login`.

### Security of the local stack

The local stack is private to your computer by default.

| Protection | What it means for you |
|---|---|
| **API keys on** | A project that you make with this version has authentication. `up --wait` makes your administrator key. A project that you made before this version, or with `--from`, has no authentication until you run `init --force`. |
| **Loopback ports** | Docker publishes ports on `127.0.0.1` only, so no other computer can connect. To open the stack on purpose, run `joulectl local init --force --bind 0.0.0.0`. Put TLS and an API key in front of it first. |
| **Own secrets** | Each project has its own random database password, cluster secret and bootstrap secret. You never need them: `joulectl local psql` connects for you. A project from before this version keeps the default database password until you run `init --force`. |
| **Private files** | `.joule/` holds your licence token and secrets. Only you can read it, and it ignores itself in git. Never commit it. |
| **Trusted projects only** | A `.joule/` folder that `joulectl local init` did not make on this computer (for example one from a `git clone`), or whose files changed, does not run until you read it and add `--trust`. |
| **Image lock** | `up` saves the ID of each image in `.joule/images.lock`. If an image changes, `up` stops. Accept the change with `--trust-images`, or refresh on purpose with `--pull`. |
| **Least privilege** | Containers drop all Linux capabilities except those that they need, and cannot gain new ones. |
| **Signed images (optional)** | Require a valid cosign signature on every image: `joulectl config update --verify-images --cosign-key cosign.pub`. |

---

## Upgrading

### Before you upgrade

> [!CAUTION]
> **Read this table before you upgrade from an earlier release.** These changes can stop a system that worked before.

| Change | What happens | What you do |
|---|---|---|
| **A licence token is necessary** | `local up` does not start the stack without a valid token. | Install a token first (`joulectl init --token`). |
| **Minimum image tag** | `--image-tag` accepts `1.3.0` and above, plus `latest` and other names that are not versions. A project with an older tag does not start. | Use a newer tag. |
| **Guardian refuses a request that has no key** | When a bootstrap secret is set, every request needs a valid key (status `401`). A script, a CI job or a program that calls Guardian must send a key. | Make a key for it (see [Add a service account](#add-a-service-account-for-a-script-or-ci-job)). |
| **An old Studio or an old joulectl cannot sign in** | They do not send a key. | Upgrade Studio and joulectl **before** you set a bootstrap secret. |
| **A key is necessary to replace the licence** | `license apply` and `init --token` need `license:admin`. | Sign in with an administrator key. |
| **Ports are private** | Docker ports are open on `127.0.0.1` only. | Use `--bind 0.0.0.0` on purpose, with TLS and a key. |
| **Unknown projects do not start** | A `.joule/` folder that this computer did not make needs `--trust`. | Read the files, then add `--trust`. |
| **Changed images stop the start** | `up` stops when an image ID differs from `.joule/images.lock`. | If you expect the change, add `--trust-images`. |
| **New secrets** | A project from before this version has the default database password and no other secret. | Run `joulectl local init --force`. The data volume stays. |
| **Services are not public** (Kubernetes) | The Services of Guardian, the daemon and Studio are `ClusterIP`. They were `LoadBalancer`. | Use an Ingress with TLS, or `kubectl port-forward`. |
| **JMX and the Java debugger are closed** | The daemon opens JMX on `127.0.0.1` only. The debugger is off. | Connect with `kubectl port-forward` or `docker exec`. |
| **The network policy refuses unknown pods** (Kubernetes) | A pod that is not in the policy cannot connect to the `joule` namespace. | Add a rule for it. Your network plugin must support network policies. The default minikube plugin does not. |

### Upgrade steps for the local stack

1. Get a licence token from Fractalworks (<https://www.fractalworks.io>).
2. Install the new version: `pip install --upgrade joulectl`
3. Make sure that the version is correct: `joulectl --version`
4. Install the token: `joulectl init --token @joule-licence.jwt`
5. Stop the stack: `joulectl local down`
6. Make the project files again: `joulectl local init --force`
7. Start the stack. The command makes your administrator key: `joulectl local up --wait`
8. Make sure that the stack runs: `joulectl local status`
9. Make sure that you are signed in: `joulectl whoami`
10. Print the key for Studio: `joulectl local key`
11. Open <http://127.0.0.1:3000> and paste the key in the sign-in dialog.

---

## Troubleshooting

Run `joulectl local doctor` first. It checks your tools, licence, sign-in and images.

### Sign-in and access errors

| Message | What to do |
|---|---|
| `An API key is required. Send it in the X-API-Key header.` (401, `AUTH_REQUIRED`) | Run `joulectl login`, or set `JOULE_API_KEY`. |
| `The API key is not valid.` (`AUTH_INVALID`) | The key is wrong or not complete. Get the key again. |
| `The API key has expired. Ask for a new key.` (`AUTH_EXPIRED`) | Ask an administrator for a new key. On the local stack, run `joulectl local login`. |
| `The API key has been revoked.` (`AUTH_REVOKED`) | Ask an administrator for a new key. |
| `This request needs the scope '<scope>', which the API key does not have.` (403, `AUTH_FORBIDDEN`) | Ask an administrator for a key that has the scope. |
| `Guardian cannot check the API key now because its key store is not available.` (503) | Make sure that the database runs, then try again. |
| `There is no such endpoint.` (404, `AUTH_UNKNOWN_ROUTE`) | Examine the path of the request. |
| `This API key can make keys for its own owner only.` (`APIKEY_OTHER_OWNER`) | Remove `--owner`, or use a key with `apikeys:admin`. |
| `This API key cannot give scopes that it does not hold:` (`APIKEY_SCOPE_NOT_HELD`) | Ask for fewer scopes, or use a key that holds them. |
| `A new key cannot last longer than the key that makes it` (`APIKEY_OUTLIVES_CREATOR`) | Use a shorter `--ttl`. |
| `The ttl cannot be more than <n> days.` (`TTL_TOO_LONG`) | Use a shorter `--ttl`. |
| `--ttl must be a number and h, d or w.` | Use a value such as `4h`, `7d` or `4w`. |
| `No API key for <host>.` | Run `joulectl login`. |
| `This Guardian does not use API keys (authentication is off).` | Set `JOULE_BOOTSTRAP_KEY` on Guardian and restart it. |
| `No key matches '<name>'. See joulectl apikey ls.` | Use an id or a prefix from the list. |
| `'<name>' matches <n> keys. Give more of the id.` | Add more characters of the id. |
| `Guardian refused the bootstrap secret` | An administrator key is active. Sign in with that key: `joulectl login`. |
| `Guardian wants one and this project has no bootstrap secret.` | Run `joulectl login`, or `joulectl local init --force`. |

### Licence errors

See [Licence problems](#licence-problems).

### Local stack problems

| Symptom | What to try |
|---|---|
| `Refusing to run an unverified project.` | Read the files that it lists, then add `--trust`. Expected for a project made by an older joulectl. |
| `An image changed since this project locked it` | A tag now points at a different image. If you expect that (a new release), use `up --trust-images`. |
| `Refusing to start: image signatures could not be verified` | Signature checks are on and an image is unsigned or built locally. Use `up --pull`, or turn the check off: `joulectl config update --no-verify-images`. |
| "port is already allocated" | Another program uses 9080, 5432 or 3000. Run `joulectl local init --force` with `--rest-port`, `--postgres-port` or `--ui-port`. The Kafka ports default to 19092 and 18081; choose others with `--kafka-port` and `--schema-registry-port`. |
| `ls` fails straight after `up` | Guardian is still starting. Use `joulectl local up --wait`. |
| Guardian or the daemon is not `(healthy)` | Wait up to a minute, then run `joulectl local logs guardian` or `joulectl local logs daemon`. |
| `Malformed transport specification` or `Failure parsing stream dsl file` | If the specification names your own class, look for `ClassNotFoundException` in `joulectl local logs daemon`, then run `joulectl local userlib add YOUR.jar`. Otherwise examine the top-level keys of the specification. |
| Studio keeps asking for a key | The key ended or was revoked. Run `joulectl local login`, then `joulectl local key`. |
| `joulectl local key` says `No API key is stored for <host:port>` | No key was made yet, or joulectl is pointed at a different host. Run `joulectl local login`, then `joulectl local key`. If Guardian refuses the bootstrap secret, an administrator key is active: sign in with `joulectl login`. |
| Cannot reach the stack from another computer | By design it listens on `127.0.0.1` only. See [Security of the local stack](#security-of-the-local-stack). |
| `TLS error talking to Joule server` | joulectl cannot check the certificate. Pass `--ca-bundle FILE`, and examine the host name. Use `--insecure` only as a last resort. |
| `No kube-context named '<profile>'` (minikube) | Start the profile (`minikube start -p <profile>`), or use another: `joulectl local init --force --minikube-profile NAME`. |
| `ErrImageNeverPull` or images not available (minikube) | The images are not in minikube. Build or pull them, then run `up`. After a rebuild, use `up --load-images`. |
| New tables or features are missing after an upgrade | Run `joulectl local reset`. |
| The project has no Studio, daemon or health checks | An older joulectl made it. Run `joulectl local down`, then `joulectl local init --force`, then `up`. |
| minikube: the REST endpoint does not respond | The port-forward may have stopped. `joulectl local status` shows it, `.joule/logs/port-forward.log` says why, and `joulectl local up` starts it again. |
| What is running? | Docker: `docker ps --filter name=joule-local`. minikube: `kubectl -n joule get pods,svc,pvc`. |

---

## Known limitations

- **AI model.** The Use Case Assistant needs an AI model. Without one, Studio shows a demo generator whose results you cannot deploy.
- **No single sign-on.** Guardian has no user accounts, passwords, OIDC or SAML. A key is the only way to sign in. The owner of a key is only a name that Guardian does not check.
- **HTTP by default.** Guardian serves HTTP until you put HTTPS in front of it. On HTTP, a key crosses the network in clear text. The local stack uses HTTP and plain gRPC on `127.0.0.1` only.
- **Older Guardian images.** An image of Guardian from before API keys does not have them. `joulectl login` then shows `This Guardian does not use API keys`.
- **No audit line for each request,** and no limit on failed sign-ins.
- **Revoke delay.** A revoke on one Guardian takes up to 30 seconds on another Guardian.
- **Studio sessions.** Studio keeps the key for one browser tab. Closing the tab signs you out.
- **Roles can change** in a later release. Make keys with scopes if you need a fixed set.
- **`config update --api-key`** sends one key to every host. Use `joulectl login` instead.
- **minikube** has less testing than Docker. If the REST endpoint does not respond, read `.joule/logs/port-forward.log`.
- **Signed images.** No signed images are published yet. Do not turn on `--verify-images` until they are.

---

## Resources

- Joule [website](https://getjoule.io)
- User and developer [documentation](https://docs.fractalworks.io/joule)
- Joule community on [Discord](https://discord.com/channels/1080521605786116196/1080521606247493724)
- Licences and commercial support: <https://www.fractalworks.io>
- Report a security problem: email lyndon.adams@fractalworks.io. Do not open a public issue.

## License

joulectl is released under the MIT License. A Joule platform licence token (see [Licence](#licence)) is required to run the Joule platform itself.

