Configure multi-tenant isolation with local and cloud tenants.
Help:
{ "command": "cloud tenant list" }viasystemprompt_helpRequires: Credentials configured -> See Credentials Playbook
Tenants are isolated environments that own databases and configuration.
StoredTenant Struct
Source: crates/infra/cloud/src/tenants.rs:32-66
pub struct StoredTenant {
#[validate(length(min = 1))]
pub id: String, // Line 35 - Unique identifier
#[validate(length(min = 1))]
pub name: String, // Line 38 - Display name
pub app_id: Option<String>, // Line 41 - Fly.io app ID
pub hostname: Option<String>, // Line 44 - Deployment hostname
pub region: Option<String>, // Line 47 - Deployment region
pub database_url: Option<String>, // Line 50 - Local DB URL
pub internal_database_url: Option<String>, // Line 53 - Cloud internal URL
#[serde(default)]
pub tenant_type: TenantType, // Line 56 - Local | Cloud
#[serde(default)]
pub external_db_access: bool, // Line 59 - Allow external connections
pub sync_token: Option<String>, // Line 62 - Sync authentication
pub shared_container_db: Option<String>, // Line 65 - Shared DB reference
}
Field Details
| Field | Type | Local Tenants | Cloud Tenants |
|---|---|---|---|
id |
String | local_abc123 |
UUID |
name |
String | User-defined | User-defined |
app_id |
Option | None | sp-{tenant-id} |
hostname |
Option | None | {tenant-id}.systemprompt.io |
region |
Option | None | iad, lhr, syd |
database_url |
Option | PostgreSQL URL | None (use internal) |
internal_database_url |
Option | None | Internal Fly.io URL |
tenant_type |
TenantType | Local |
Cloud |
external_db_access |
bool | true/false | Usually false |
sync_token |
Option | Optional | Required for sync |
shared_container_db |
Option | Container name | None |
TenantType Enum
pub enum TenantType {
#[default]
Local, // Development tenant on local machine
Cloud, // Production tenant on SystemPrompt Cloud
}
TenantStore Struct
Source: crates/infra/cloud/src/tenants.rs:226-327
pub struct TenantStore {
#[validate]
pub tenants: Vec<StoredTenant>, // Line 229 - All registered tenants
pub synced_at: DateTime<Utc>, // Line 231 - Last sync timestamp
}
Storage Location
The tenant store is saved to tenants.json:
# profile.yaml
cloud:
tenants_path: "../../tenants.json" # Relative to profile dir
Default location: .systemprompt/tenants.json
Tenant Store Methods
Loading
impl TenantStore {
pub fn load_from_path(path: &Path) -> Result<Self> {
if !path.exists() {
return Err(CloudError::TenantsNotSynced);
}
let content = fs::read_to_string(path)?;
let store: TenantStore = serde_json::from_str(&content)?;
// Validate all tenants
store.validate()?;
Ok(store)
}
}
Saving
impl TenantStore {
pub fn save_to_path(&self, path: &Path) -> Result<()> {
// Ensure parent directory exists with .gitignore
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
fs::write(parent.join(".gitignore"), "*")?;
}
}
let content = serde_json::to_string_pretty(self)?;
fs::write(path, &content)?;
// Set Unix permissions to 0o600 (owner read/write only)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
}
Ok(())
}
}
Finding Tenants
impl TenantStore {
pub fn find_tenant(&self, id: &str) -> Option<&StoredTenant> {
self.tenants.iter().find(|t| t.id == id)
}
pub fn is_empty(&self) -> bool {
self.tenants.is_empty()
}
pub fn len(&self) -> usize {
self.tenants.len()
}
}
Staleness Check
impl TenantStore {
pub fn is_stale(&self, max_age: Duration) -> bool {
Utc::now() - self.synced_at > max_age
}
}
Creating Tenants
Local Tenant
impl StoredTenant {
pub fn new_local(id: String, name: String, database_url: String) -> Self {
Self {
id,
name,
database_url: Some(database_url),
tenant_type: TenantType::Local,
..Default::default()
}
}
pub fn new_local_shared(
id: String,
name: String,
database_url: String,
shared_container_db: String
) -> Self {
Self {
id,
name,
database_url: Some(database_url),
shared_container_db: Some(shared_container_db),
tenant_type: TenantType::Local,
..Default::default()
}
}
}
Cloud Tenant
pub struct NewCloudTenantParams {
pub id: String,
pub name: String,
pub app_id: String,
pub hostname: String,
pub region: String,
pub internal_database_url: Option<String>,
pub sync_token: Option<String>,
}
impl StoredTenant {
pub fn new_cloud(params: NewCloudTenantParams) -> Self {
Self {
id: params.id,
name: params.name,
app_id: Some(params.app_id),
hostname: Some(params.hostname),
region: Some(params.region),
internal_database_url: params.internal_database_url,
sync_token: params.sync_token,
tenant_type: TenantType::Cloud,
..Default::default()
}
}
}
State Query Methods
Source: crates/infra/cloud/src/tenants.rs:159-224
impl StoredTenant {
// Check if using shared PostgreSQL container
pub fn uses_shared_container(&self) -> bool {
self.shared_container_db.is_some()
}
// Check if has local database URL
pub fn has_database_url(&self) -> bool {
self.database_url.is_some()
}
// Get local database URL
pub fn get_local_database_url(&self) -> Option<&String> {
self.database_url.as_ref()
}
// Type checks
pub fn is_cloud(&self) -> bool {
matches!(self.tenant_type, TenantType::Cloud)
}
pub fn is_local(&self) -> bool {
matches!(self.tenant_type, TenantType::Local)
}
// Cloud credential checks
pub fn is_sync_token_missing(&self) -> bool {
self.is_cloud() && self.sync_token.is_none()
}
pub fn is_database_url_masked(&self) -> bool {
self.database_url
.as_ref()
.map(|url| url.contains(":***@"))
.unwrap_or(false)
}
pub fn has_missing_credentials(&self) -> bool {
self.is_cloud() && (
self.is_sync_token_missing() ||
!self.is_database_url_masked()
)
}
}
tenants.json Format
{
"tenants": [
{
"id": "local_19bff27604c",
"name": "my-project",
"tenant_type": "local",
"database_url": "postgres://systemprompt:localdev@localhost:5432/systemprompt",
"external_db_access": false,
"shared_container_db": "systemprompt-shared-db"
},
{
"id": "999bc654-9a64-49bc-98be-db976fc84e76",
"name": "my-project-prod",
"tenant_type": "cloud",
"app_id": "sp-999bc6549a64",
"hostname": "999bc6549a64.systemprompt.io",
"region": "iad",
"internal_database_url": "postgres://user:pass@internal-db:5432/tenant",
"sync_token": "sp_sync_abc123..."
}
],
"synced_at": "2026-02-01T10:00:00Z"
}
Linking Tenants to Profiles
Profiles reference tenants via cloud.tenant_id:
# .systemprompt/profiles/local/profile.yaml
cloud:
tenant_id: local_19bff27604c
tenants_path: "../../tenants.json"
Resolution Flow
- Profile specifies
cloud.tenant_id - Load TenantStore from
cloud.tenants_path - Find tenant by ID:
TenantStore::find_tenant(id) - Use tenant's database URL or internal URL
Local vs Cloud Tenant Differences
| Aspect | Local Tenant | Cloud Tenant |
|---|---|---|
| Database | Docker container | Managed PostgreSQL |
| URL | database_url field |
internal_database_url field |
| Hostname | localhost:port |
{id}.systemprompt.io |
| Sync token | Optional | Required for sync |
| App ID | None | Fly.io app ID |
| Region | None | iad, lhr, syd |
| External access | Configurable | Usually disabled |
Sync Token
Cloud tenants use sync tokens for authenticated synchronization:
{
"id": "999bc654-...",
"sync_token": "sp_sync_abc123..."
}
Sync Token Usage
- Required for
systemprompt cloud sync push/pull - Validates write access to tenant
- Rotatable via
systemprompt cloud tenant rotate-sync-token
Missing Sync Token Check
if tenant.is_sync_token_missing() {
warn!("Sync token missing for cloud tenant {}", tenant.id);
}
File Security
Permissions
Tenant files are saved with restricted permissions:
#[cfg(unix)]
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
- Owner: read + write
- Group: none
- Other: none
Gitignore
Parent directory automatically gets .gitignore:
if !parent.exists() {
fs::create_dir_all(parent)?;
fs::write(parent.join(".gitignore"), "*")?;
}
CLI Commands
List Tenants
systemprompt cloud tenant list
Show Tenant Details
systemprompt cloud tenant show
systemprompt cloud tenant show <tenant-id>
Create Tenant
# Local tenant
systemprompt cloud tenant create --type local --name my-project
# Cloud tenant
systemprompt cloud tenant create --region iad --name my-project
Select Tenant
systemprompt cloud tenant select <tenant-id>
Rotate Credentials
systemprompt cloud tenant rotate-credentials <tenant-id> -y
systemprompt cloud tenant rotate-sync-token <tenant-id> -y
Troubleshooting
"Tenants not synced"
- Run
systemprompt cloud tenant listto sync - Verify credentials are valid
"Tenant not found"
- Check tenant ID in profile's
cloud.tenant_id - Run
systemprompt cloud tenant listto refresh
"Database connection failed"
- For local: ensure Docker container is running
- For cloud: verify
internal_database_urlis set
"Sync token missing"
- Cloud tenants require sync token for sync operations
- Run
systemprompt cloud tenant rotate-sync-token
"Permission denied"
- Check file permissions on tenants.json
- Should be
0600(owner read/write only)
Quick Reference
| Task | Command |
|---|---|
| List tenants | systemprompt cloud tenant list |
| Show tenant | systemprompt cloud tenant show <id> |
| Create local | systemprompt cloud tenant create --type local |
| Create cloud | systemprompt cloud tenant create --region iad |
| Select tenant | systemprompt cloud tenant select <id> |
| Rotate creds | systemprompt cloud tenant rotate-credentials <id> |
| Rotate sync | systemprompt cloud tenant rotate-sync-token <id> |
| Delete tenant | systemprompt cloud tenant delete <id> -y |