Configure cloud authentication tokens and API validation.
Help:
{ "command": "cloud auth whoami" }viasystemprompt_helpRequires: Profile configured -> See Profiles Playbook
CloudCredentials authenticate CLI and API requests to SystemPrompt Cloud. Optional for local-only deployments.
CloudCredentials Struct
Source: crates/infra/cloud/src/credentials.rs:13-25
pub struct CloudCredentials {
#[validate(length(min = 1))]
pub api_token: String, // Line 16 - JWT token
#[validate(url)]
pub api_url: String, // Line 19 - Cloud API endpoint
pub authenticated_at: DateTime<Utc>, // Line 21 - Login timestamp
#[validate(email)]
pub user_email: String, // Line 24 - User email
}
Field Details
| Field | Type | Validation | Description |
|---|---|---|---|
api_token |
String | Non-empty | JWT token from OAuth login |
api_url |
String | Valid URL | Cloud API endpoint (default: https://api.systemprompt.io) |
authenticated_at |
DateTime | - | When the token was issued |
user_email |
String | Valid email | User's email address |
CredentialsBootstrap
Source: crates/infra/cloud/src/credentials_bootstrap.rs
Static Storage
static CREDENTIALS: OnceLock<Option<CloudCredentials>> = OnceLock::new();
Note: Stores Option<CloudCredentials> because cloud credentials are optional.
Initialization (Async)
pub async fn init() -> Result<Option<&'static CloudCredentials>> {
if std::env::var("FLY_APP_NAME").is_ok() {
// Fly.io container: load from environment
let creds = Self::load_from_env()?;
Self::validate_with_api(&creds).await?;
Ok(CREDENTIALS.get_or_init(|| Some(creds)).as_ref())
} else {
// Local: load from credentials.json
let path = Self::get_credentials_path()?;
let creds = CloudCredentials::load_from_path(&path)?;
Self::check_expiration(&creds)?;
Self::validate_with_api(&creds).await?;
Ok(CREDENTIALS.get_or_init(|| Some(creds)).as_ref())
}
}
Error Types
pub enum CredentialsBootstrapError {
NotInitialized, // Not yet loaded
AlreadyInitialized, // Already initialized
NotAvailable, // Cloud not configured
FileNotFound { path }, // credentials.json missing
InvalidCredentials { message }, // Parse/validation error
TokenExpired, // Token older than 24 hours
ApiValidationFailed { message }, // Cloud API rejected token
}
Token Expiration
Source: crates/infra/cloud/src/credentials.rs:140-149
Cloud tokens expire 24 hours after authentication.
Expiration Check
impl CloudCredentials {
pub fn expires_within(&self, duration: Duration) -> bool {
let expires_at = self.authenticated_at + Duration::hours(24);
Utc::now() + duration >= expires_at
}
pub fn is_expired(&self) -> bool {
self.expires_within(Duration::zero())
}
}
Expiration Handling
| Condition | Behavior |
|---|---|
| Expired | Error: TokenExpired |
| Expires within 1 hour | Warning logged |
| Valid | Continue normally |
File-Based Loading
credentials.json Format
{
"api_token": "sp_live_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"api_url": "https://api.systemprompt.io",
"authenticated_at": "2026-02-01T10:00:00Z",
"user_email": "user@example.com"
}
File Location
The credentials file path comes from the profile:
# profile.yaml
cloud:
credentials_path: "../../credentials.json" # Relative to profile dir
Default location: .systemprompt/credentials.json
Loading Code
impl CloudCredentials {
pub fn load_from_path(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)
.map_err(|_| FileNotFound { path })?;
let creds: CloudCredentials = serde_json::from_str(&content)
.map_err(|e| InvalidCredentials { message: e.to_string() })?;
creds.validate()?; // Uses validator crate
Ok(creds)
}
}
Environment-Based Loading
When running in a Fly.io container, credentials load from environment variables.
Environment Variables
| Env Variable | Required | Default |
|---|---|---|
SYSTEMPROMPT_API_TOKEN |
Yes | - |
SYSTEMPROMPT_USER_EMAIL |
Yes | - |
SYSTEMPROMPT_API_URL |
No | https://api.systemprompt.io |
Loading Code
Source: crates/infra/cloud/src/credentials_bootstrap.rs:96-116
fn load_from_env() -> Result<CloudCredentials> {
let api_token = std::env::var("SYSTEMPROMPT_API_TOKEN")
.map_err(|_| InvalidCredentials {
message: "SYSTEMPROMPT_API_TOKEN not set".to_string()
})?;
let user_email = std::env::var("SYSTEMPROMPT_USER_EMAIL")
.map_err(|_| InvalidCredentials {
message: "SYSTEMPROMPT_USER_EMAIL not set".to_string()
})?;
let api_url = std::env::var("SYSTEMPROMPT_API_URL")
.unwrap_or_else(|_| "https://api.systemprompt.io".to_string());
Ok(CloudCredentials {
api_token,
api_url,
authenticated_at: Utc::now(), // Set to now for env-based
user_email,
})
}
API Validation
After loading, credentials are validated against the Cloud API.
Source: crates/infra/cloud/src/credentials_bootstrap.rs:81-90
async fn validate_with_api(credentials: &CloudCredentials) -> Result<()> {
let client = CloudApiClient::new(
&credentials.api_url,
&credentials.api_token
);
client.get_user().await
.map_err(|e| ApiValidationFailed {
message: format!("API validation failed: {}", e)
})?;
Ok(())
}
What is Validated
- Token is not revoked
- Token has correct permissions
- API endpoint is reachable
Fly.io Container Detection
fn is_fly_container() -> bool {
std::env::var("FLY_APP_NAME").is_ok()
}
When FLY_APP_NAME is set:
- Skip file-based loading
- Load from environment variables
authenticated_atis set to current time
Access Methods
Get Credentials
// Get optional credentials (Ok(None) if not configured)
CredentialsBootstrap::get() -> Result<Option<&'static CloudCredentials>>
// Require credentials (error if not configured)
CredentialsBootstrap::require() -> Result<&'static CloudCredentials>
// Check if initialized
CredentialsBootstrap::is_initialized() -> bool
// Idempotent initialization
CredentialsBootstrap::try_init() -> Result<Option<&'static CloudCredentials>>
// Reload from file (ignores OnceLock)
CredentialsBootstrap::reload() -> Result<CloudCredentials>
Static Expiration Check
// Check if credentials expire within duration
CredentialsBootstrap::expires_within(duration: Duration) -> bool
OAuth Login Flow
Credentials are created through OAuth login:
# Terminal-based login
just login
# Or direct CLI
systemprompt cloud auth login
Login Process
- Open browser to OAuth provider (GitHub/Google)
- User authenticates with provider
- Callback to local server with code
- Exchange code for SystemPrompt API token
- Save to
.systemprompt/credentials.json
What Gets Saved
{
"api_token": "sp_live_...",
"api_url": "https://api.systemprompt.io",
"authenticated_at": "2026-02-01T10:00:00Z",
"user_email": "user@example.com"
}
Profile Configuration
Configure cloud credentials path in profile:
# .systemprompt/profiles/local/profile.yaml
cloud:
credentials_path: "../../credentials.json"
tenants_path: "../../tenants.json"
tenant_id: local_19bff27604c
validation: strict # strict | warn | skip
Validation Modes
| Mode | Missing File | Invalid Token | API Failure |
|---|---|---|---|
strict |
Error | Error | Error |
warn |
Warning | Warning | Warning |
skip |
Silent | Silent | Silent |
Troubleshooting
"Credentials not initialized"
- Run
just loginto authenticate - Check credentials.json exists
"Token expired"
- Re-authenticate:
just login - Tokens expire after 24 hours
"API validation failed"
- Check network connectivity
- Verify api_url is correct
- Token may be revoked
"File not found"
- Verify path in profile's
cloud.credentials_path - Path is relative to profile directory
"Invalid credentials"
- Check JSON syntax
- Verify all required fields present
- Validate email format
Security Best Practices
- Never commit: Add
.systemprompt/credentials.jsonto.gitignore - File permissions: Set to
0600(owner read/write only) - Token rotation: Re-login periodically
- Separate credentials: Don't share between environments
- Revoke on compromise: Contact support to revoke tokens
Quick Reference
| Task | Command / Location |
|---|---|
| Login | just login |
| Check auth | systemprompt cloud auth whoami |
| Logout | just logout |
| View credentials | .systemprompt/credentials.json |
| Set in container | SYSTEMPROMPT_API_TOKEN, SYSTEMPROMPT_USER_EMAIL |
| API endpoint | SYSTEMPROMPT_API_URL (default: https://api.systemprompt.io) |