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:
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:
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)
import os, time, uuidimport 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
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:
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
- Keep the secret secret: generate
HELIOSDB_JWT_SECRETwith at least 32 random bytes and store it in your secret manager, not in source control. - Short expiry: mint tokens with a short
exp(for example one hour) and mint a new one when it lapses. - 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.
- Browsers: cross-origin requests are refused unless you list exact origins in
HELIOSDB_CORS_ALLOWED_ORIGINS(comma-separated;*is ignored). - No trust auth in production: use
--auth scram-sha-256with--passwordon 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 |