Authentication Model
Two Types of Auth
Relay has two separate authentication systems:
- Dashboard Auth — for humans logging into the web dashboard
- WebSocket Auth — for apps and agents connecting to Relay's API
They use different methods because they have different needs.
Dashboard Authentication
Email + OTP (No Passwords)
The Relay dashboard uses email + one-time password (OTP) for all logins.
1. User enters email
↓
2. Relay sends 6-digit OTP via AWS SES
↓
3. User receives email and enters OTP
↓
4. Relay verifies OTP
↓
5. User is logged in, Relay issues JWT
Why No Passwords?
Passwords are:
- Hard to remember
- Easy to crack if weak
- Prone to reuse across sites
- Add complexity to account recovery
OTP is:
- Simple and secure
- Sent via email (which the user controls)
- Time-limited (10 minutes)
- Rate-limited (5 attempts max before lockout)
OTP Details
| Property | Value |
|---|---|
| Length | 6 digits |
| Expiry | 10 minutes |
| Max attempts | 5 incorrect entries |
| Lockout duration | 15 minutes (after 5 failed attempts) |
After 5 incorrect OTP entries, you're locked out for 15 minutes. Request a new OTP to reset the counter.
JWT (JSON Web Token)
After entering a valid OTP, Relay issues a JWT token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoiam...
This JWT:
- Is stored in your browser's session storage or cookies
- Authenticates all dashboard API calls
- Expires after a period of inactivity (typically 7 days)
- Cannot be used on WebSocket connections (different auth)
WebSocket Authentication
Bearer Token on Handshake
Apps and agents authenticate to Relay's WebSocket using Bearer tokens on handshake.
WebSocket Connection Request:
URL: wss://api.relay.ckgworks.com/v1/ws/app
Headers:
Authorization: Bearer rlk_portal_x8k2m9p...
When the connection is established:
- Relay extracts the token from the Authorization header
- Relay verifies the token (checks bcrypt hash against DB)
- If valid → connection is accepted
- If invalid → connection is rejected immediately
Token Types
| Type | Prefix | Used By | Example |
|---|---|---|---|
| App token | rlk_ | Applications sending events | rlk_portal_x8k2m... |
| Agent token | rla_ | Agents/plugins receiving events | rla_athena_k9x2p... |
One-Time Auth
Auth happens once per connection, on handshake. After that:
- ✅ All messages on the connection are trusted
- ✅ No per-message signing required
- ✅ No HMAC headers needed
This is the Telegram model — authenticate on connect, then trust the channel.
Why No HMAC?
HMAC (Hash-based Message Authentication Code) is used when:
- You have stateless HTTP connections
- Each request must prove authenticity independently
- You can't trust the channel itself
Example without HMAC (bad):
POST https://relay.example.com/event
Body: { agent_id: "athena", payload: {...} }
Headers:
Authorization: Bearer rlk_portal_...
X-HMAC-SHA256: 5d41402abc4b2a76b9719d911017c592...
Every request needs the HMAC header.
But Relay uses WebSocket (stateful, persistent connection):
- ✅ Channel is authenticated once on handshake
- ✅ All subsequent messages on the channel are trusted
- ✅ No need to re-prove identity for each message
- ✅ HMAC would add complexity with no benefit
Example with Relay (good):
WebSocket Connect:
Authorization: Bearer rlk_portal_... ← Authenticate once
Send event:
{ type: "event", agent_id: "athena", payload: {...} }
← No additional auth needed, channel is trusted
Token Management
Token Storage
Relay stores tokens as bcrypt hashes, not plaintext.
When you create a token:
Plaintext token: rlk_portal_x8k2m9p2d7q4v1r9z5k3j8h2m7n4q1...
Bcrypt hash: $2b$12$R9h.cIPz0gi.URNNGI3OXeRDz8P.dwOuq3D3/wLhBkWkGJmJCxByO
Storage: Hash is stored in DB, plaintext is never saved
When authenticating:
1. Client sends token
2. Relay hashes it
3. Relay compares hash with stored hash
4. If match → authentication succeeds
This means:
- ✅ Even if the database is compromised, tokens can't be extracted
- ✅ Plaintext tokens are shown only at creation time
- ✅ Lost tokens cannot be recovered from Relay
Plaintext Display (One-Time Only)
When you create an app or agent token, it's shown once:
Token created! Copy it now — you won't see it again.
rlk_portal_x8k2m9p2d7q4v1r9z5k3j8h2m7n4q1...
[Copy] [Done]
After you click away:
- ❌ The plaintext is not displayed again
- ❌ It cannot be retrieved from the dashboard
- ✅ You must store it securely (
.env, secrets manager, etc.)
Per-Connection vs. Shared Tokens
Apps and Agents
When you register an app or agent, you get one token per resource.
App: Portal
Token: rlk_portal_x8k2m9p...
↓
Can have multiple instances (load-balanced)
├── API Server 1 (uses same token)
├── API Server 2 (uses same token)
└── API Server 3 (uses same token)
↓
Each instance maintains its own WebSocket connection
Similarly for agents:
Agent: Athena
Token: rla_athena_k9x2p...
↓
OpenClaw plugin connects with this token
└── One WebSocket connection per token
Multiple Tokens (Token Rotation)
If you rotate a token:
Old token: rlk_portal_x8k2m9p... (1-hour grace period)
New token: rlk_portal_q8l2m9r...
Old connections can use old token for 1 hour
New connections must use new token
This allows:
- ✅ Gradual migration from old to new token
- ✅ No immediate service disruption
- ✅ Time to deploy the new token to all instances
Security Implications
Token Exposure
If a token is exposed:
-
In code: Someone can authenticate as that app/agent
- They can send events (if app token) or receive events (if agent token)
- They can potentially see other apps' payloads (agent) or impersonate the app
- Fix: Rotate the token immediately
-
In logs: Check if the token was logged anywhere
- Grep your application logs:
grep -r "rlk_portal" logs/ - Clear any files that contain the token
- Consider them compromised and rotate
- Grep your application logs:
-
In environment: If an environment variable is exposed
- Rotate the token
- Review who had access to that environment
- Update your secrets management
Best Practice: Treat Tokens Like Passwords
- ✅ Store in environment variables (
.env, secrets manager) - ✅ Rotate periodically (monthly or quarterly)
- ✅ Never commit to version control
- ✅ Never share via email or chat
- ✅ Use
.gitignorefor.envfiles - ❌ Don't log tokens
- ❌ Don't embed in code
- ❌ Don't share with contractors without a formal agreement
Organization Isolation in Auth
Apps Can't Escape Their Organization
An app token is bound to a specific organization:
Organization A:
App: Portal (rlk_portal_orgA_...)
Organization B:
App: Portal (rlk_portal_orgB_...)
Portal's token in Organization A cannot access Organization B's resources
When authenticating:
- Relay hashes the token
- Relay looks up which organization owns this token
- All subsequent operations are scoped to that organization
- Apps can only see agents in their own organization
This is enforced at the database level — impossible to escape.
Authentication Checklist
When integrating with Relay:
- Store your app/agent token securely (env vars, secrets manager)
- Use HTTPS/WSS for all connections (never HTTP/WS)
- Verify the token works (test connection from your app/agent)
- Don't log the token
- Rotate the token if it's exposed or periodically
- Use the correct endpoint URL (api.relay.ckgworks.com)
- Set the Authorization header correctly (Bearer token)
Summary
- Dashboard: Email + OTP, no passwords
- WebSocket: Bearer token on handshake
- Tokens: Stored as bcrypt hashes, shown once at creation
- No HMAC: WebSocket channel is already authenticated
- Organization-scoped: Tokens bound to their organization
- Rotation: New token issued, old has 1-hour grace period
Next steps: