Flow Studio
Credential Management
How messaging nodes resolve secrets — the ICredentialResolver contract, credential ID references, and the mandatory no-raw-secrets rule.
The Rule
Mandatory: All messaging secrets — Slack bot tokens, SMTP passwords, SendGrid API keys, Twilio Account SID/Auth Token pairs, FCM service account JSON, APNs p8 keys — are stored and retrieved exclusively via
ICredentialResolver. Node configuration stores only an integer credentialId. Raw secrets in node config are a critical security defect.
ICredentialResolver
public interface ICredentialResolver
{
/// Retrieve the secret value for the given credential ID.
/// Always returns the decrypted secret string.
/// Throws CredentialNotFoundException if the ID does not exist.
Task<string> GetPasswordAsync(int credentialId, CancellationToken cancellationToken = default);
}
Credential Storage per Channel
| Channel | Stored Value | Notes |
|---|---|---|
| Slack | Bot OAuth token (xoxb-...) | Single string |
| Email (SMTP) | SMTP password | Username stored in non-secret config; only password is secret |
| Email (SendGrid) | SendGrid API key (SG.xxx) | Single string |
| SMS (Twilio) | JSON: {"accountSid":"AC...","authToken":"..."} | Both values needed; stored as JSON object |
| SMS (Vonage) | JSON: {"apiKey":"...","apiSecret":"..."} | Both values needed; stored as JSON object |
| Push (FCM) | Full service account JSON key | Multi-line JSON; stored as-is |
| Push (APNs) | JSON: {"p8Key":"...","teamId":"...","keyId":"...","bundleId":"..."} | All 4 fields needed |
Registering a Credential
Credentials are registered via the Credential Management API (not by hand-editing the database):
POST /api/credentials
Content-Type: application/json
{
"name": "Acme Slack Bot Token",
"type": "messaging",
"channel": "slack",
"tenantId": "tenant-acme",
"secretValue": "xoxb-your-actual-bot-token-here"
}
// Response:
{
"credentialId": 12,
"name": "Acme Slack Bot Token",
"type": "messaging",
"createdAt": "2026-05-25T08:00:00Z"
}
The returned credentialId (e.g., 12) is what you place in the node's credentialId field.
Runtime Resolution
// Inside any IMessagingChannelAdapter.SendAsync():
var secret = await _credentials.GetPasswordAsync(config.CredentialId, ct);
// For channels that store composite JSON:
var twilioAuth = JsonSerializer.Deserialize<TwilioCredential>(secret)!;
TwilioClient.Init(twilioAuth.AccountSid, twilioAuth.AuthToken);
Credential Rotation
When a secret is rotated (e.g., Slack token revoked and reissued), update the stored credential value via:
PUT /api/credentials/12/secret
{
"secretValue": "xoxb-new-token-value"
}
No node configuration changes are required — the credentialId integer remains the same. All running and future executions automatically use the new secret on the next call to GetPasswordAsync.
Audit trail: The credential management system maintains an audit log of all secret access events keyed by
credentialId, execution ID, and actor. This provides a complete access history without the secret value ever appearing in logs.