Authentication

j17 uses different auth methods depending on what you're doing. API keys for data operations, JWT for admin tasks. Each environment (prod/staging/test) has isolated credentials.

API Keys

Use API keys for instance data: writing events, reading aggregates, batch operations.

curl https://myapp.j17.dev/order/abc123 \
  -H "Authorization: Bearer j17_0_prod_abc123xyz"

Key format

j17_{version}_{environment}_{random}
Prefix Environment Works on
j17_0_prod_* Production *.j17.dev
j17_0_staging_* Staging *-staging.j17.dev
j17_0_test_* Test *-test.j17.dev

Keys are environment-scoped. A staging key won't work on production. This prevents accidents.

Creating keys

Via dashboard: Instance settings page, API Keys section.

Via API (requires JWT, on the headnode):

curl -X POST https://control.j17.dev/api/instances/$INSTANCE_ID/keys \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production Backend",
    "scope": "write",
    "environment": "prod"
  }'

Key scopes

Scope Permissions
read GET aggregates, query events, read projections
write POST events + all read permissions

Use read keys for frontend clients that only display data. Use write keys for backend services.

Rotating keys

curl -X POST https://control.j17.dev/api/keys/$KEY_ID/rotate \
  -H "Authorization: Bearer $JWT_TOKEN"

You can also schedule revocation with a grace period:

curl -X POST https://control.j17.dev/api/keys/$KEY_ID/schedule_revocation \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"revoke_at": "2024-02-01T00:00:00Z"}'

Storing keys

Don't commit keys to git. Use environment variables:

# .env
J17_API_KEY=j17_0_prod_abc123xyz
// Read from env, not hardcoded
const apiKey = process.env.J17_API_KEY;

Never expose write keys in frontend code. Browser clients should use read-only keys or go through your backend.

JWT Tokens

Use JWT for admin operations: managing API keys, deploying specs, configuring instances. All JWT-authenticated endpoints are on the headnode (control plane).

curl https://control.j17.dev/api/instances \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Getting a token

Login with email/password on the headnode:

curl -X POST https://control.j17.dev/api/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@example.com",
    "password": "..."
  }'

Response:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": 1705312800
}

Tokens expire after 24 hours. Refresh by logging in again.

Token contents

j17 JWTs contain:

{
  "sub": "user-uuid",
  "email": "admin@example.com",
  "role": "instance_admin",
  "instance_id": "instance-uuid",
  "iat": 1705226400,
  "exp": 1705312800
}

Don't decode JWTs client-side to make permission decisions. Always verify server-side.

Environment isolation

Each environment is completely isolated:

Environment URL Credentials
Production myapp.j17.dev j17_0_prod_* keys
Staging myapp-staging.j17.dev j17_0_staging_* keys
Test myapp-test.j17.dev j17_0_test_* keys

Data doesn't flow between environments. A user created in staging doesn't exist in production.

Testing against staging

# Always hit staging first
curl https://myapp-staging.j17.dev/user/abc123/was_created \
  -H "Authorization: Bearer $STAGING_KEY" \
  -d '{...}'

# Verify it works, then switch to production
curl https://myapp.j17.dev/user/abc123/was_created \
  -H "Authorization: Bearer $PROD_KEY" \
  -d '{...}'

Rate limits

Scope Limit
Per API key 500 requests/minute
Per IP 2,000 requests/minute

Platform admins are exempt from rate limiting.

Rate limit headers on every response:

X-RateLimit-Limit: 500
X-RateLimit-Remaining: 499
X-RateLimit-Scope: api-key

Error responses

Invalid key

{
  "ok": false,
  "error": "Invalid API key"
}

HTTP 401. Check the key is correct and not revoked.

Wrong environment

{
  "ok": false,
  "error": "Staging key cannot access production"
}

HTTP 403. You're using a staging key on production (or vice versa).

Insufficient scope

{
  "ok": false,
  "error": "Read-only key cannot write events"
}

HTTP 403. Get a write-scoped key or use a different endpoint.

Expired JWT

{
  "ok": false,
  "error": "Token expired"
}

HTTP 401. Log in again to get a fresh token.

Best practices

Use separate keys per service. Don't share one key across frontend, backend, and admin tools. If one leaks, you only rotate that one.

Read keys for frontend. If your browser code needs to fetch aggregates, use a read-only key. Better yet, proxy through your backend.

Monitor usage. Dashboard shows which keys are active. Disable unused keys.

Rotate regularly. Set a calendar reminder. Quarterly rotation catches leaked keys before they're exploited.

Never log keys. If you debug HTTP requests, redact the Authorization header.

Node secrets (internal)

If you're self-hosting j17 workers, internal communication (headnode-to-worker) uses node secrets:

X-Node-Secret: {random-long-string}

You don't need this for normal API usage. It's for the infrastructure layer between headnode and worker nodes.

See also

Can't find what you need? support@j17.app