> For the complete documentation index, see [llms.txt](https://docs.k9.io/key9-identity/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.k9.io/key9-identity/jsonair/2-install/2.5-configuration.md).

# 2.5 Configuring and Running JSONAir

JSONAir is configured entirely through environment variables. There are no configuration files to edit. Variables can be provided in two ways:

* A `.env` file placed in the same directory as the binary
* System environment variables (set via `export`, a process manager, or a container runtime)

If a `.env` file is present, it is loaded automatically at startup. System environment variables take precedence if both are set.

***

## Environment Variables

### Required — Database

| Variable         | Description             | Example          |
| ---------------- | ----------------------- | ---------------- |
| `MYSQL_USERNAME` | Database username       | `jsonair`        |
| `MYSQL_PASSWORD` | Database password       | `s3cr3tpassword` |
| `MYSQL_HOST`     | Database hostname or IP | `127.0.0.1`      |
| `MYSQL_PORT`     | Database port           | `3306`           |
| `MYSQL_DATABASE` | Database name           | `jsonair`        |

### Optional — Database TLS

| Variable                | Description                                                                            | Default |
| ----------------------- | -------------------------------------------------------------------------------------- | ------- |
| `MYSQL_TLS`             | Set to `true` to enable TLS for the database connection                                | `false` |
| `MYSQL_TLS_SKIP_VERIFY` | Set to `true` to disable certificate verification. **Not recommended for production.** | `false` |

### Required — HTTP Server

| Variable      | Description                                                           | Example                   |
| ------------- | --------------------------------------------------------------------- | ------------------------- |
| `HTTP_LISTEN` | Address and port to listen on                                         | `:9191` or `0.0.0.0:9191` |
| `HTTP_MODE`   | Server mode. Must be one of: `production`, `release`, `debug`, `test` | `production`              |

> **Note:** `production` and `release` are equivalent — both run the Gin framework in release mode with HTTP access logging suppressed. Use `debug` during development for verbose request logging.

### Optional — HTTPS (TLS)

If `HTTP_TLS` is not set or is `false`, the server listens on plain HTTP.

| Variable    | Description                      | Example                                           |
| ----------- | -------------------------------- | ------------------------------------------------- |
| `HTTP_TLS`  | Set to `true` to enable HTTPS    | `true`                                            |
| `HTTP_CERT` | Path to the TLS certificate file | `/etc/letsencrypt/live/example.com/fullchain.pem` |
| `HTTP_KEY`  | Path to the TLS private key file | `/etc/letsencrypt/live/example.com/privkey.pem`   |

When `HTTP_TLS=true`, both `HTTP_CERT` and `HTTP_KEY` are required.

### Required — Security

| Variable                | Description                                                                                                                                             | Example                |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| `JWT_TOKEN_SECRET`      | Secret used to sign and verify JWTs. Use a long, random string.                                                                                         | `openssl rand -hex 32` |
| `JWT_TOKEN_EXPIRE`      | JWT lifetime in **minutes**                                                                                                                             | `60`                   |
| `TOKEN_HMAC_SECRET`     | Secret used to HMAC-SHA256 hash PATs before storing or comparing them. Must match the value used when inserting keys into the database.                 | `openssl rand -hex 32` |
| `CONFIG_ENCRYPT_SECRET` | Secret used to derive the AES-256-GCM key for decrypting `config_data` at rest. Must match the secret used when encrypting data with `jsonair-encrypt`. | `openssl rand -hex 32` |

### Required — Process

| Variable | Description                                              | Example  |
| -------- | -------------------------------------------------------- | -------- |
| `RUNAS`  | Username to drop privileges to after binding to the port | `nobody` |

***

## Example `.env` File

Create a file named `.env` in the same directory as the `jsonair` binary. A minimal production setup looks like this:

```ini
# Database
MYSQL_USERNAME=jsonair
MYSQL_PASSWORD=your-strong-db-password
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_DATABASE=jsonair
MYSQL_TLS=false

# HTTP
HTTP_LISTEN=:9191
HTTP_MODE=production
HTTP_TLS=false

# Security
JWT_TOKEN_SECRET=replace-with-output-of-openssl-rand-hex-32
JWT_TOKEN_EXPIRE=60
TOKEN_HMAC_SECRET=replace-with-a-different-openssl-rand-hex-32
CONFIG_ENCRYPT_SECRET=replace-with-a-third-openssl-rand-hex-32

# Process
RUNAS=nobody
```

For HTTPS, add:

```ini
HTTP_TLS=true
HTTP_CERT=/etc/letsencrypt/live/example.com/fullchain.pem
HTTP_KEY=/etc/letsencrypt/live/example.com/privkey.pem
```

> **Security:** Never commit a `.env` file containing real credentials to source control. Add `.env` to your `.gitignore`.

***

## Starting the Server

### Directly

If using a `.env` file, simply run the binary from the same directory:

```bash
cd /opt/jsonair
./jsonair
```

### Using export.sh

A convenience script is included at `cmd/jsonair/scripts/export.sh`. Copy and edit it with your values, then run it to set environment variables and start the server in one step:

```bash
cp cmd/jsonair/scripts/export.sh /opt/jsonair/start.sh
# Edit start.sh with your values
chmod +x /opt/jsonair/start.sh
./start.sh
```

### As a systemd Service

For production Linux deployments, run JSONAir as a systemd service. Create `/etc/systemd/system/jsonair.service`:

```ini
[Unit]
Description=JSONAir Configuration Server
After=network.target mysql.service
Wants=mysql.service

[Service]
Type=simple
User=root
WorkingDirectory=/opt/jsonair
EnvironmentFile=/etc/jsonair/jsonair.env
ExecStart=/opt/jsonair/jsonair
Restart=on-failure
RestartSec=5
KillMode=mixed
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target
```

Place your environment variables in `/etc/jsonair/jsonair.env` (same format as a `.env` file), then enable and start the service:

```bash
sudo systemctl daemon-reload
sudo systemctl enable jsonair
sudo systemctl start jsonair
sudo systemctl status jsonair
```

> **Note:** The service starts as `root` so it can bind to the configured port, then immediately drops privileges to the user specified in `RUNAS`. This is the intended behavior.

***

## Verifying Startup

On a successful start you should see output similar to:

```
Banner :jsonair: -*> JSONAir! <*-
Banner :jsonair: Version: 1.0.0
Banner :jsonair: Champ Clark III & The Key9, Inc. Team [https://k9.io]
Banner :jsonair: Copyright (C) 2026 Key9, Inc. et al.
Info   :jsonair: Listening on ':9191' for traffic as UID: 65534.
```

The UID shown (`65534` on most Linux systems is `nobody`) confirms that privilege drop succeeded.

***

## Health Check

JSONAir exposes an unauthenticated health endpoint at `/health`. It pings the database and returns HTTP `200` when healthy or HTTP `503` when the database is unreachable.

```bash
curl http://localhost:9191/health
```

Healthy response:

```json
{"status": "ok"}
```

Unhealthy response (HTTP 503):

```json
{"status": "error", "message": "database unreachable"}
```

This endpoint is intentionally unauthenticated so that load balancers, orchestrators, and container runtimes can poll it without credentials.

### Docker HEALTHCHECK

```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:9191/health || exit 1
```
