---
title: "Authentication Guide"
description: "How HeliosDB Lite authenticates PostgreSQL wire clients (trust, password, SCRAM) and REST callers (HS256 JWTs signed with HELIOSDB_JWT_SECRET)."
---

# Authentication Guide

> PostgreSQL roles on the wire, signed JWTs on REST

---

## Overview

`heliosdb-lite start` serves two network surfaces, and each one authenticates on its own:

| Surface | Default address | How clients authenticate |
|---------|-----------------|--------------------------|
| PostgreSQL wire | `127.0.0.1:5432` | `--auth trust` (default), `password`, `md5` or `scram-sha-256` |
| REST API (`/v1`, `/api/v1`) | `127.0.0.1:8080` | `Authorization: Bearer <JWT>`, an HS256 token signed with `HELIOSDB_JWT_SECRET` |

Both surfaces run SQL as a database role, so `GRANT` privileges and row-level security apply to the caller. Superusers bypass row-level security. The default bootstrap superusers are `postgres` and `helios`.

---

## PostgreSQL Wire

The default `--auth trust` accepts every connection without a password. Any client can then claim any role, including `postgres`. Use it for local development only. For anything shared, start the server with SCRAM:

```bash
export HELIOSDB_JWT_SECRET="$(openssl rand -hex 32)"
heliosdb-lite start --data-dir ./data --auth scram-sha-256 --password 'choose-a-strong-password'
```

`--password` sets the password of the bootstrap users `postgres` and `helios`:

```bash
psql "postgresql://postgres@127.0.0.1:5432/heliosdb"
```

Add `--tls-cert` and `--tls-key` (PEM files) to encrypt PostgreSQL wire connections. Row-level security protects data only when clients authenticate as non-superuser roles with `password`, `md5` or `scram-sha-256`.

---

## REST API: JWT Bearer Tokens

### Configure the signing secret

The REST API refuses to start unless `HELIOSDB_JWT_SECRET` is set. The server validates tokens with that secret; it does not issue them. Lite has no sign-up, login or refresh endpoint. A built-in token service is planned. Until then, your application or identity service mints the tokens.

### Required claims

| Claim | Value |
|-------|-------|
| `sub` | The database role the request runs as (privileges and row-level security use it) |
| `tenant_id` | Tenant identifier string |
| `client_id` | A UUID identifying the client |
| `iat`, `nbf`, `exp` | Issued-at, not-before and expiry, as Unix timestamps |
| `jti` | Unique token ID |
| `iss` | `heliosdb-api` |
| `aud` | `heliosdb-client` |
| `scopes` | Array of strings (may be empty) |

The algorithm is HS256. Tokens with a wrong issuer or audience, an expired `exp`, or a future `nbf` are rejected with `401`.

### Mint a token (Python)

```python
import os, time, uuid
import jwt  # pip install PyJWT

now = int(time.time())
token = jwt.encode(
    {
        "sub": "app_user",            # a role that exists in the database
        "tenant_id": "default",
        "client_id": str(uuid.uuid4()),
        "iat": now, "nbf": now, "exp": now + 3600,
        "jti": str(uuid.uuid4()),
        "iss": "heliosdb-api",
        "aud": "heliosdb-client",
        "scopes": [],
    },
    os.environ["HELIOSDB_JWT_SECRET"],
    algorithm="HS256",
)
print(token)
```

### Use the token

```bash
export TOKEN="$(python3 mint_token.py)"

curl -X POST http://127.0.0.1:8080/v1/branches/main/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT current_user"}'
```

Create the role named in `sub` first, over the PostgreSQL wire as a superuser, and grant it what it needs:

```sql
CREATE ROLE app_user LOGIN PASSWORD 'another-strong-password';
GRANT SELECT, INSERT, UPDATE, DELETE ON users TO app_user;
```

### API keys

The REST middleware also accepts an `X-API-Key` header, but only for keys that an application registers when it embeds the REST server. The stock `heliosdb-lite start` registers no API keys, so use JWTs with it.

---

## Multi-Tenant Authentication

Because `sub` becomes the database role, row-level security policies see the REST caller exactly as they see a PostgreSQL wire session for that role. Issue one role per tenant, or per application user, and write policies against `current_user`. Do not mint tokens for superusers for tenant traffic, because superusers bypass row-level security.

---

## Security Best Practices

1. **Keep the secret secret**: generate `HELIOSDB_JWT_SECRET` with at least 32 random bytes and store it in your secret manager, not in source control.
2. **Short expiry**: mint tokens with a short `exp` (for example one hour) and mint a new one when it lapses.
3. **TLS in front of REST**: the REST port serves plain HTTP. Put a TLS-terminating reverse proxy in front of it for any non-local traffic.
4. **Browsers**: cross-origin requests are refused unless you list exact origins in `HELIOSDB_CORS_ALLOWED_ORIGINS` (comma-separated; `*` is ignored).
5. **No trust auth in production**: use `--auth scram-sha-256` with `--password` on the PostgreSQL wire.

---

## Troubleshooting

| Response | Cause | Fix |
|----------|-------|-----|
| `401 {"error":"Unauthorized","message":"Missing or invalid authentication credentials"}` | No `Authorization` header | Send `Authorization: Bearer <token>` |
| `401 {"error":"Unauthorized","message":"Invalid or expired JWT token"}` | Wrong secret, issuer or audience; expired or not yet valid | Re-mint the token with the claims above |
| Server exits at start with a `HELIOSDB_JWT_SECRET` error | Secret not set | Export `HELIOSDB_JWT_SECRET` before `heliosdb-lite start` |
| Query returns permission errors | The `sub` role lacks privileges | `GRANT` the privileges to that role |

---

## API Reference

- [REST API Reference](https://www.heliosdb.com/docs/lite/website/api/readme/)
- [Quick Start](https://www.heliosdb.com/docs/lite/website/getting-started/quickstart/)
