Skip to main content

Token Management

Token Lifecycle

Every token goes through several stages:

  1. Creation — Relay generates the token, shows it once
  2. Storage — You store it securely
  3. Use — Your app/agent uses it to authenticate
  4. Rotation — You issue a new token, retire the old one

Creation

When you register an app or agent:

Dashboard: Click "Register New App"

Relay: Generates token (random cryptographic string)

Dashboard: Shows token ONCE

You: Copy and store immediately

The token is:

  • Generated randomly (cryptographically secure)
  • Hashed with bcrypt
  • Stored in the database as a hash only
  • Never logged or stored in plaintext

Storage

Once you have a token, store it securely:

Local Development

Use .env files (gitignored):

# .env
RELAY_APP_TOKEN=rlk_portal_x8k2m9p...
RELAY_AGENT_TOKEN_ATHENA=rla_athena_k9x2p...

In your code:

import os
token = os.getenv("RELAY_APP_TOKEN")

Important: Add .env to .gitignore:

# .gitignore
.env
.env.local
.env.*.local

Verify it worked:

git status # should NOT show .env

Production

Use a secrets manager:

  • AWS Secrets ManagerGetSecretValue("relay/tokens")
  • HashiCorp Vaultvault kv get relay/tokens
  • Kubernetes Secretskubectl get secret relay-tokens
  • Azure Key Vaultaz keyvault secret show --name relay-tokens
  • Environment variables — deployed by CI/CD, never checked in

Example with AWS:

import boto3
client = boto3.client('secretsmanager', region_name='us-west-2')
response = client.get_secret_value(SecretId='relay/tokens')
token = response['SecretString']

Example with Kubernetes:

apiVersion: v1
kind: Secret
metadata:
name: relay-tokens
type: Opaque
stringData:
app-token: rlk_portal_x8k2m9p...
agent-token: rla_athena_k9x2p...
---
apiVersion: v1
kind: Pod
metadata:
name: portal-api
spec:
containers:
- name: api
env:
- name: RELAY_APP_TOKEN
valueFrom:
secretKeyRef:
name: relay-tokens
key: app-token

Use

Your app/agent uses the token to authenticate:

App Example (Python)

import asyncio
import websockets
import json

async def connect_to_relay():
uri = "wss://api.relay.ckgworks.com/v1/ws/app"
token = os.getenv("RELAY_APP_TOKEN")

# Auth header on handshake
headers = {
"Authorization": f"Bearer {token}"
}

async with websockets.connect(uri, subprotocols=["authorization"], extra_headers=headers) as ws:
# Connection established, token verified

# Send event
event = {
"type": "event",
"agent_id": "athena",
"thread_id": "task-123",
"payload": {"message": "Hello agent"}
}
await ws.send(json.dumps(event))

# Receive response
reply = await ws.recv()
print(reply)

Agent Example (TypeScript)

import WebSocket from 'ws';

const token = process.env.RELAY_AGENT_TOKEN_ATHENA;
const ws = new WebSocket('wss://api.relay.ckgworks.com/v1/ws/agent', {
headers: {
Authorization: `Bearer ${token}`
}
});

ws.on('open', () => {
console.log('Connected to Relay');
// Token verified, ready to receive events
});

ws.on('message', (data) => {
const message = JSON.parse(data);
console.log('Event received:', message);

// Send reply
const reply = {
type: "reply",
event_id: message.event_id,
content: "Hello from Athena"
};
ws.send(JSON.stringify(reply));
});

What Happens on Auth Failure

If the token is invalid:

WebSocket Connection Request:
Authorization: Bearer rlk_portal_INVALID...

Relay: Invalid token, connection rejected

Error: WebSocket connection failed (4000 - Unauthorized)

Your code should handle this gracefully:

try:
async with websockets.connect(uri, extra_headers=headers) as ws:
# use connection
except websockets.exceptions.InvalidStatusException as e:
if e.status == 4000: # Unauthorized
print("Token is invalid or expired")
# Rotate token, reload config, retry

Rotation

If you suspect a token is compromised, or if you rotate tokens periodically:

Step 1: Generate New Token

In the Relay dashboard:

  1. Go to Apps → click your app
  2. Click Rotate Token
  3. New token is shown (copy immediately!)

The old token now enters a 1-hour grace period.

Step 2: Update Your Configuration

Update wherever you store the token:

Local .env:

# .env
RELAY_APP_TOKEN=rlk_portal_NEW_TOKEN... # changed

Secrets Manager (AWS):

aws secretsmanager update-secret \
--secret-id relay/tokens \
--secret-string '{"app_token":"rlk_portal_NEW_TOKEN..."}'

Kubernetes:

kubectl delete secret relay-tokens
kubectl create secret generic relay-tokens \
--from-literal=app-token=rlk_portal_NEW_TOKEN...

Step 3: Redeploy Your Service

Deploy the updated configuration:

# Local: restart your development server
python api.py

# Production: redeploy with new env
docker build -t myapp:v2 .
docker push myapp:v2
kubectl set image deployment/portal-api api=myapp:v2

Step 4: Monitor Connections

During the 1-hour grace period:

  • ✅ Both old and new tokens work
  • ✅ Existing connections on old token stay alive
  • ✅ New connections use new token
  • ✅ If you restart, new connections will use new token

After 1 hour:

  • ❌ Old token stops working
  • ✅ Only new token is valid

Timeline

T+0:00 Token rotation initiated
New token issued, old has grace period

T+0:05 .env updated in source
T+0:10 New deployment pushed
T+0:15 New instances start using new token

T+0:30 Most traffic on new token
Some old connections still active

T+1:00 Grace period expires
Old token completely invalid

No disruption — connections gracefully shift to new token.

Compromise Response

If a token is exposed:

Immediate (Within 5 minutes)

  1. Rotate the token in the Relay dashboard
  2. Copy the new token
  3. Update your environment (.env, secrets manager, etc.)
  4. Redeploy immediately

Short-term (Within 1 hour)

  1. Monitor event logs for unusual activity
  2. Check who had access to the exposed token
  3. Review recent events — was the token used by an attacker?
  4. Ensure the grace period hasn't expired (grace period is 1 hour)

Long-term

  1. Document the incident for compliance/audit
  2. Review security practices — how did the token get exposed?
  3. Implement monitoring to catch future exposures

Token Security Checklist

Development

  • Token stored in .env (not in code)
  • .env is in .gitignore
  • Token not logged anywhere
  • .env not committed to git (verify with git log)
  • Team members store tokens locally in their own .env
  • Token not shared via Slack, email, or Discord

Production

  • Token stored in secrets manager (not in code)
  • Secrets manager access is restricted (audit logs)
  • Token not in Docker image layers
  • Token not in environment variable logs
  • Token not in application logs
  • Secrets rotated every 90 days (or on compromise)
  • Rotation has no downtime (grace period configured)

Rotation Schedule

Rotate tokens periodically, even if not compromised:

FrequencyReason
MonthlyIf token exposure is likely (shared environments, many people with access)
QuarterlyStandard security practice
AnnuallyMinimal exposure risk (dedicated servers, few people)
On compromiseImmediately

Access Control

  • Only authorized people have access to tokens
  • Contractors/vendors get temporary tokens (rotated after engagement)
  • Departing employees — revoke token access immediately
  • All access is logged (who accessed the secret, when)

Common Mistakes

Mistake: Committing Tokens to Git

git add .env # WRONG — .env has tokens
git commit

How to fix:

git rm --cached .env
echo ".env" >> .gitignore
git commit -m "Remove .env from tracking"
git push

# Assume token is compromised, rotate it

Even if you delete it from history, it's still in older commits. If the repo is public or shared, rotate the token.

Mistake: Logging Tokens

logging.info(f"Connecting with token {token}") # WRONG

How to fix:

logging.info("Connecting to Relay...") # SAFE
# Redact tokens in logs
token_masked = token[:10] + "..." + token[-5:]
logging.debug(f"Token: {token_masked}")

Mistake: Storing in Code

TOKEN = "rlk_portal_x8k2m9p..." # WRONG

How to fix:

TOKEN = os.getenv("RELAY_APP_TOKEN") # RIGHT

Mistake: Sharing Tokens with Team

Don't share tokens via email, Slack, or screen sharing.

How to fix:

  • Each team member stores their own copy from the dashboard
  • Use a secrets manager for shared infrastructure
  • Never verbally recite a token

Summary

  • Create tokens in the dashboard
  • Store securely: .env (dev), secrets manager (prod)
  • Never log or commit tokens
  • Use tokens on WebSocket handshake
  • Rotate periodically (monthly, quarterly, or annually)
  • Rotate immediately if exposed
  • 1-hour grace period allows graceful migration
  • Monitor access to token storage

Next steps: