Token Management
Token Lifecycle
Every token goes through several stages:
- Creation — Relay generates the token, shows it once
- Storage — You store it securely
- Use — Your app/agent uses it to authenticate
- 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 Manager —
GetSecretValue("relay/tokens") - HashiCorp Vault —
vault kv get relay/tokens - Kubernetes Secrets —
kubectl get secret relay-tokens - Azure Key Vault —
az 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:
- Go to Apps → click your app
- Click Rotate Token
- 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)
- Rotate the token in the Relay dashboard
- Copy the new token
- Update your environment (
.env, secrets manager, etc.) - Redeploy immediately
Short-term (Within 1 hour)
- Monitor event logs for unusual activity
- Check who had access to the exposed token
- Review recent events — was the token used by an attacker?
- Ensure the grace period hasn't expired (grace period is 1 hour)
Long-term
- Document the incident for compliance/audit
- Review security practices — how did the token get exposed?
- Implement monitoring to catch future exposures
Token Security Checklist
Development
- Token stored in
.env(not in code) -
.envis in.gitignore - Token not logged anywhere
-
.envnot committed to git (verify withgit 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:
| Frequency | Reason |
|---|---|
| Monthly | If token exposure is likely (shared environments, many people with access) |
| Quarterly | Standard security practice |
| Annually | Minimal exposure risk (dedicated servers, few people) |
| On compromise | Immediately |
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: