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
- Writing events - POST with API keys
- Reading aggregates - GET with API keys
- Admin API - Requires JWT