Skip to main content

Secret Management API

The Secret Management API provides a secure, hierarchical system for managing encrypted configuration secrets across your sites and applications. It features automatic encryption, schema validation, JSON schema validation, and a 2-tier inheritance model.

Overview​

The Secret Management system enables you to:

  • Classify Secret Types: Distinguish between system-provided and custom secret types
  • Define Schemas: Create secret types with JSON Schema validation
  • 2-Tier Inheritance: Site → App-level secrets with automatic fallback
  • Automatic Encryption: Private and sensitive secrets are encrypted at rest
  • Version History: Track all changes with full audit trails (excludes actual values for security)
  • Optional Authentication: Public secrets accessible without authentication
  • Caching: High-performance Redis caching for secret retrieval
  • Flexible Tags: Tag-based system for categorizing and filtering secrets

Architecture​

Key Concepts​

Secret Hierarchy​

Secrets follow a 2-tier inheritance model:

  1. Site Level: Scoped to a specific tenant/site (shared across all apps)
  2. App Level: Scoped to a specific application within a site

When retrieving a secret, the system searches: App → Site (first match wins)

Sensitivity Levels​

Each secret type has an immutable sensitivity level:

  • public: Plaintext storage, accessible without authentication

    • Use for: Non-sensitive configuration (feature flags, public API endpoints)
  • private: Encrypted storage, requires authentication

    • Use for: API keys, service credentials
  • sensitive: Encrypted storage with additional security

    • Use for: Database passwords, signing keys, tokens
warning

Sensitivity level is immutable after creation and inherited from the secret type.

Secret Types​

Secret types define the structure and validation rules for secrets with a classification system:

  • Type: Classification - system or custom (extensible for future types)
    • system: Built-in types that cannot be deleted (protected)
    • custom: User-defined types that can be managed freely
  • Name: Unique identifier (e.g., database_config, jwt_signing_key)
  • Schema: JSON Schema for validation
  • Sensitivity Level: Determines encryption and access control
  • Description: Human-readable documentation

Example Secret Type:

{
"type": "custom",
"name": "database_config",
"slug": "database-config",
"description": "Database connection configuration",
"sensitivity_level": "private",
"schema": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"},
"database": {"type": "string"},
"username": {"type": "string"},
"password": {"type": "string"}
},
"required": ["host", "port", "database"]
}
}

Type Classification​

Secret types are classified into two categories:

  • System Types ("type": "system"):

    • Pre-defined by the platform
    • Cannot be deleted (protected)
    • Can be updated (name, description, schema)
    • Type classification is immutable after creation
  • Custom Types ("type": "custom"):

    • User-defined
    • Can be fully managed (created, updated, deleted when not in use)
    • Type classification is immutable after creation
warning

Both sensitivity_level and type classification are immutable after creation. Plan your secret type structure carefully.

Tags​

Tags provide a flexible way to categorize and filter secrets:

  • Name: Human-readable label (e.g., "production", "staging", "critical")
  • Slug: Auto-generated URL-friendly identifier
  • Color: Visual identification color code in UI
  • Many-to-Many: Each secret can have multiple tags

Tags help you:

  • Categorize secrets by environment (production, staging, development)
  • Group by service type (database, api, authentication)
  • Mark priority levels (critical, optional)
  • Filter secrets in the admin interface and API queries

Base URL Structure​

Site-Level Secrets (Tenant-specific)​

/api/secrets/                    # Site-level secrets
/api/secrets/?app={app_slug} # Filter by app
/api/secrets/?key={key} # Filter by key

Secret Types & Tags​

# Site-level
/api/secret-types/
/api/tags/

Authentication​

All endpoints require authentication except:

  • GET /api/secrets/{key}/ - Returns only public secrets when unauthenticated

Authentication Methods​

JWT Bearer Token (Recommended for API clients)

Authorization: Bearer YOUR_ACCESS_TOKEN

Browser Session (Web browsers)

Cookie: sessionid=YOUR_SESSION_ID

API Endpoints​

Secret Types​

List Secret Types​

Request:

GET /api/secret-types/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Query Parameters:

ParameterTypeDescription
searchstringSearch by name or description (partial, case-insensitive)
typestringFilter by type classification: system or custom
sensitivity_levelstringFilter by sensitivity: public, private, or sensitive
orderingstringSort field (prefix with - for descending). Default: name
pageintPage number (default: 1)
page_sizeintItems per page (default: 20, max: 100)

Available Ordering Fields:

  • name - Sort by secret type name (default)
  • type - Sort by classification (system/custom)
  • sensitivity_level - Sort by sensitivity level
  • created_at - Sort by creation timestamp
  • modified_at - Sort by last modification timestamp

Response:

{
"items": [
{
"type": "custom",
"name": "database_config",
"slug": "database-config",
"description": "Database connection configuration",
"sensitivity_level": "private",
"schema": { /* JSON Schema */ }
},
{
"type": "system",
"name": "jwt_signing_key",
"slug": "jwt-signing-key",
"description": "JWT token signing key",
"sensitivity_level": "sensitive"
}
],
"count": 25,
"next": "http://your-tenant-domain.com/api/secret-types/?page=2",
"previous": null
}

Filter & Search Examples:

# Search by name or description
GET /api/secret-types/?search=database
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Filter by type classification
GET /api/secret-types/?type=system
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Filter by sensitivity level
GET /api/secret-types/?sensitivity_level=private
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Combine search with filters
GET /api/secret-types/?search=api&type=custom&sensitivity_level=private
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Sorting Examples:

# Sort by name ascending (default)
GET /api/secret-types/?ordering=name
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Sort by creation date descending (newest first)
GET /api/secret-types/?ordering=-created_at
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Sort by sensitivity level ascending
GET /api/secret-types/?ordering=sensitivity_level
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Combine filters, search, sorting and pagination
GET /api/secret-types/?search=config&type=custom&ordering=-created_at&page=1&page_size=10
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Create Secret Type (Custom)​

POST /api/secret-types/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
"type": "custom",
"name": "api_credentials",
"description": "API credentials for external services",
"sensitivity_level": "private",
"schema": {
"type": "object",
"properties": {
"api_key": {"type": "string"},
"api_secret": {"type": "string"}
},
"required": ["api_key"]
}
}

Response: 201 Created

{
"status": "success",
"message": "Secret type created successfully",
"data": {
"type": "custom",
"name": "api_credentials",
"slug": "api-credentials",
"sensitivity_level": "private"
}
}

Create Secret Type (System)​

POST /api/secret-types/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
"type": "system",
"name": "jwt_signing_key",
"description": "JWT token signing key",
"sensitivity_level": "sensitive"
}

Get Secret Type​

GET /api/secret-types/{slug}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Response:

{
"type": "custom",
"name": "api_credentials",
"slug": "api-credentials",
"description": "API credentials for external services",
"sensitivity_level": "private"
}

Update Secret Type (PUT)​

PUT /api/secret-types/{slug}/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
"name": "api_credentials",
"description": "Updated: API credentials for integrations",
"schema": {
"type": "object",
"properties": {
"api_key": {"type": "string"},
"api_secret": {"type": "string"},
"environment": {"type": "string"}
},
"required": ["api_key"]
}
}
note
  • sensitivity_level is immutable and cannot be updated
  • type classification is immutable and cannot be changed
  • name can be updated if no naming conflicts exist
  • description and schema can be freely updated
  • All fields in the payload are optional (partial updates supported)

Delete Secret Type​

DELETE /api/secret-types/{slug}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Response: 200 OK

{
"message": "Secret type deleted successfully",
"detail": "Deleted secret type: api_credentials"
}

Error Response (System Type): 400 Bad Request

{
"error": "System secret types cannot be deleted."
}

Error Response (Type in Use): 400 Bad Request

{
"error": "This secret type is used by 5 secret(s) and cannot be deleted."
}

Secrets​

List All Secrets​

GET /api/secrets/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Query Parameters:

ParameterTypeDescription
searchstringSearch by key (partial, case-insensitive match)
keystringFilter by exact secret key
appstringFilter by app slug
tagsstringComma-separated tag slugs (OR logic)
secret_typestringFilter by secret type slug
created_bystringFilter by creator username
orderingstringSort field (prefix with - for descending). Default: -created_at
pageintPage number (default: 1)
page_sizeintItems per page (default: 20, max: 100)

Available Ordering Fields:

  • key - Sort by secret key
  • created_at - Sort by creation timestamp (default: -created_at)
  • modified_at - Sort by last modification timestamp
  • type__name - Sort by secret type name
  • created_by__username - Sort by creator username

Response:

{
"items": [
{
"key": "stripe_api_key",
"tags": ["production", "payment"],
"secret_type": "api_credentials",
"value": {
"api_key": "sk_live_12345",
"api_secret": "secret_67890"
}
}
],
"count": 50,
"next": "http://your-tenant-domain.com/api/secrets/?page=2",
"previous": null
}

Search & Filter Examples:

# Search by key (partial match)
GET /api/secrets/?search=stripe
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Filter by exact key
GET /api/secrets/?key=database_url
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Filter by app slug
GET /api/secrets/?app=mobile-app
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Filter by tags (any of the provided tags)
GET /api/secrets/?tags=production,database
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Filter by secret type
GET /api/secrets/?secret_type=api-credentials
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Filter by creator
GET /api/secrets/?created_by=admin
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Combine multiple filters
GET /api/secrets/?app=mobile-app&tags=production&secret_type=api-credentials
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Sorting Examples:

# Sort by key ascending
GET /api/secrets/?ordering=key
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Sort by creation date descending (newest first, default)
GET /api/secrets/?ordering=-created_at
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Sort by secret type name
GET /api/secrets/?ordering=type__name
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Combine search, filters, sorting and pagination
GET /api/secrets/?search=api&tags=production&ordering=-created_at&page=1&page_size=20
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Get Secret History​

Retrieve the audit history for a specific secret. History tracking captures who made changes, when, and what action was performed, but excludes actual secret values for security.

Actions Tracked:

  • + = Created
  • ~ = Changed/Updated
  • - = Deleted

What is tracked:

  • Key, app, type, tags
  • Who made the change (user)
  • When it happened (timestamp)
  • What action was performed

What is NOT tracked (security):

  • ❌ Actual secret values (plaintext or encrypted)

Site-Level Secret:

GET /api/secrets/{key}/history/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

App-Level Secret:

GET /api/secrets/{key}/history/?app={app_slug}
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

With Pagination:

# Get first 5 history records
GET /api/secrets/{key}/history/?limit=5&offset=0
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Get next 5 records
GET /api/secrets/{key}/history/?limit=5&offset=5
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Response: 200 OK

{
"key": "database_url",
"app": null,
"results": [
{
"action": "~",
"user": "admin@example.com",
"timestamp": "2025-11-22T04:30:15.123Z",
"key": "database_url",
"changes": {
"tags": ["production", "database"]
}
},
{
"action": "+",
"user": "admin@example.com",
"timestamp": "2025-11-21T10:15:00.000Z",
"key": "database_url",
"changes": {}
}
],
"total_count": 2,
"limit": 10,
"offset": 0,
"has_next": false,
"has_previous": false
}

Query Parameters:

  • app - App slug for app-level secrets
  • limit - Number of records (default: 10, max: 100)
  • offset - Records to skip for pagination (default: 0)

Get Secret by Key​

Site-Level (with 2-tier inheritance):

# Authenticated - returns any secret based on permissions
GET /api/secrets/{key}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Unauthenticated - returns only public secrets
GET /api/secrets/{key}/
Host: your-tenant-domain.com

App-Level:

GET /api/secrets/{key}/?app={app_slug}
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

With Tag Validation:

# Get secret only if it has "production" OR "staging" tag
GET /api/secrets/{key}/?tags=production,staging
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

# Get app-level secret with tag validation
GET /api/secrets/{key}/?app={app_slug}&tags=production
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Query Parameters:

  • app - Filter by app slug (optional)
  • tags - Comma-separated tag names - returns 404 if secret doesn't have ANY of the specified tags (optional)

Response:

{
"key": "stripe_api_key",
"tags": ["production"],
"secret_type": "api_credentials",
"value": {
"api_key": "sk_live_12345",
"api_secret": "secret_67890"
}
}

Error Response (Unauthenticated, Private Secret): 401 Unauthorized

{
"error": "Authentication required",
"detail": "Authentication credentials were not provided. Please provide a valid JWT token or session to access this secret."
}

Error Response (Tag Mismatch): 404 Not Found

{
"error": "Secret not found",
"detail": "Secret with key 'stripe_api_key' does not have any of the required tags: production, critical"
}

Create Secret (Site-Level)​

POST /api/secrets/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
"secret_type": "api-credentials",
"key": "stripe_credentials",
"value": {
"api_key": "sk_live_abc123",
"api_secret": "secret_xyz789"
},
"tags": ["production", "payment"],
"app": null
}

Response: 201 Created

{
"key": "stripe_credentials",
"tags": ["production", "payment"],
"secret_type": "api_credentials",
"value": {
"api_key": "sk_live_abc123",
"api_secret": "secret_xyz789"
}
}

Create Secret (App-Level)​

POST /api/secrets/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
"secret_type": "api-credentials",
"key": "app_api_key",
"value": {
"api_key": "sk_test_mobile_123"
},
"tags": ["mobile", "staging"],
"app": "mobile-app"
}

Update Secret​

PUT /api/secrets/{key}/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
"secret_type": "api-credentials",
"key": "stripe_credentials",
"value": {
"api_key": "sk_live_updated_999",
"api_secret": "new_secret_888"
},
"tags": ["production"]
}

App-Level:

PUT /api/secrets/{key}/?app={app_slug}
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Response: 200 OK

{
"key": "stripe_credentials",
"tags": ["production"],
"secret_type": "api_credentials",
"value": {
"api_key": "sk_live_updated_999",
"api_secret": "new_secret_888"
}
}

Delete Secret​

DELETE /api/secrets/{key}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

App-Level:

DELETE /api/secrets/{key}/?app={app_slug}
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Response: 200 OK

{
"message": "Secret deleted successfully",
"detail": "Deleted secret with key: stripe_credentials"
}

Common Use Cases​

1. Storing Database Credentials (Private)​

Step 1: Create Custom Secret Type

POST /api/secret-types/
{
"type": "custom",
"name": "database_credentials",
"description": "Database connection credentials",
"sensitivity_level": "private",
"schema": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"},
"username": {"type": "string"},
"password": {"type": "string"}
},
"required": ["host", "username", "password"]
}
}

Step 2: Create Secret

POST /api/secrets/
{
"secret_type": "database-credentials",
"key": "postgres_main",
"value": {
"host": "db.example.com",
"port": 5432,
"username": "appuser",
"password": "secure_password_123"
},
"tags": ["database", "production"]
}

2. Public Configuration (Feature Flags)​

Step 1: Create Public Secret Type

POST /api/secret-types/
{
"type": "custom",
"name": "feature_flags",
"description": "Application feature flags",
"sensitivity_level": "public",
"schema": {
"type": "object",
"properties": {
"dark_mode": {"type": "boolean"},
"new_dashboard": {"type": "boolean"}
}
}
}

Step 2: Create Public Secret

POST /api/secrets/
{
"secret_type": "feature-flags",
"key": "app_features",
"value": {
"dark_mode": true,
"new_dashboard": false
},
"tags": ["configuration", "public"]
}

Step 3: Access Without Authentication

# Anyone can access this
GET /api/secrets/app_features/
Host: your-tenant-domain.com

Response:
{
"key": "app_features",
"tags": ["configuration", "public"],
"secret_type": "feature_flags",
"value": {
"dark_mode": true,
"new_dashboard": false
}
}

3. App-Specific API Keys​

# Each app gets its own API key
POST /api/secrets/
{
"secret_type": "api-credentials",
"key": "stripe_api_key",
"value": {"api_key": "sk_live_mobile_abc123"},
"tags": ["payment", "mobile"],
"app": "mobile-app"
}

POST /api/secrets/
{
"secret_type": "api-credentials",
"key": "stripe_api_key",
"value": {"api_key": "sk_live_web_xyz789"},
"tags": ["payment", "web"],
"app": "web-app"
}

# Retrieval follows inheritance
GET /api/secrets/stripe_api_key/?app=mobile-app
# Returns: sk_live_mobile_abc123

GET /api/secrets/stripe_api_key/?app=web-app
# Returns: sk_live_web_xyz789

4. System Secret Types (Protected)​

# Create a system type that cannot be deleted
POST /api/secret-types/
{
"type": "system",
"name": "jwt_signing_key",
"description": "JWT token signing keys - DO NOT DELETE",
"sensitivity_level": "sensitive"
}

# Try to delete it - will fail
DELETE /api/secret-types/jwt-signing-key/
# Response: 400 Bad Request
# {"error": "System secret types cannot be deleted."}

# You can update name, description, and schema
PUT /api/secret-types/jwt-signing-key/
{
"description": "Updated description for JWT keys",
"schema": {
"type": "object",
"properties": {
"key": {"type": "string"},
"algorithm": {"type": "string", "enum": ["HS256", "RS256"]}
}
}
}
# Response: 200 OK

# Trying to change type classification will fail
PUT /api/secret-types/jwt-signing-key/
{
"type": "custom"
}
# Response: 400 Bad Request
# {"error": "Type classification cannot be changed after creation."}

5. Filtering Secrets by Tags​

# Create secrets with tags
POST /api/secrets/
{
"secret_type": "database-credentials",
"key": "postgres_prod",
"value": {"host": "prod-db.example.com"},
"tags": ["production", "database", "critical"]
}

POST /api/secrets/
{
"secret_type": "api-credentials",
"key": "stripe_prod",
"value": {"api_key": "sk_live_123"},
"tags": ["production", "payment"]
}

POST /api/secrets/
{
"secret_type": "database-credentials",
"key": "postgres_staging",
"value": {"host": "staging-db.example.com"},
"tags": ["staging", "database"]
}

# Get all production secrets
GET /api/secrets/?tags=production
# Returns: postgres_prod, stripe_prod

# Get all database secrets (any environment)
GET /api/secrets/?tags=database
# Returns: postgres_prod, postgres_staging

# Get production database secrets (combine with app filter)
GET /api/secrets/?tags=production,database&app=backend-api
# Returns: Only secrets tagged with production OR database in backend-api

# Get critical production secrets
GET /api/secrets/?tags=critical,production
# Returns: postgres_prod (has both tags)

Error Responses​

400 Bad Request - Schema Validation​

{
"error": "Schema validation failed",
"detail": "host: Field required; port: Field required"
}

400 Bad Request - Duplicate Entry​

{
"message": "Secret type 'api_credentials' already exists",
"field": "name"
}

400 Bad Request - System Type Protection​

{
"error": "System secret types cannot be deleted."
}

400 Bad Request - Type Classification Immutable​

{
"error": "Type classification cannot be changed after creation.",
"detail": {
"field": "type",
"current": "custom",
"attempted": "system"
}
}

401 Unauthorized​

{
"error": "Authentication required",
"detail": "This secret requires authentication. Please provide a valid JWT token or session to access private or sensitive secrets."
}

403 Forbidden​

{
"error": "Permission denied",
"detail": "You do not have permission to access this secret"
}

404 Not Found​

{
"error": "Secret not found",
"detail": "Secret with key 'unknown_key' not found in app, site level(s)"
}

Best Practices​

1. Use Appropriate Sensitivity Levels​

  • Public: Only for truly non-sensitive data (feature flags, public endpoints)
  • Private: Default for most secrets (API keys, service credentials)
  • Sensitive: Critical secrets (database passwords, signing keys)

2. Leverage Inheritance​

  • Store shared secrets at site level
  • Store app-specific configs at app level
  • App-level secrets override site-level ones

3. Use System Types for Critical Infrastructure​

  • Mark important secret types as "type": "system" to prevent accidental deletion
  • Use for: JWT keys, database credentials, encryption keys
  • System types can still be updated (name, description, schema), just not deleted
  • Plan ahead: type classification cannot be changed after creation

4. Use Tags for Categorization​

Categorize secrets with meaningful tags for better management and filtering:

  • production, staging, development - Environment tags
  • database, api, auth - Service type tags
  • critical, optional - Priority tags
  • mobile-app, web-app - Application-specific tags

5. Define Strict Schemas​

Use comprehensive JSON schemas to:

  • Validate secret structure
  • Document required fields
  • Prevent configuration errors

6. Rotate Secrets Regularly​

Update sensitive secrets periodically:

PUT /api/secrets/api_key/
{
"secret_type": "api-credentials",
"key": "api_key",
"value": {"api_key": "new_rotated_key_xyz"},
"tags": ["production"]
}

Security Considerations​

  1. Encryption: Private and sensitive secrets are automatically encrypted at rest using PostgreSQL's pgcrypto
  2. Authentication: All write operations require authentication
  3. Permission Checks: Access is controlled via site membership and permissions
  4. Audit Trail: All changes are tracked with timestamps and user information (excludes actual values)
  5. Schema Validation: JSON Schema validation prevents malformed secret values
  6. Immutable Fields: Both sensitivity_level and type classification cannot be changed after creation
  7. System Protection: System secret types cannot be deleted
  8. Delete Protection: Secret types cannot be deleted if secrets are using them

Tags API​

Tags are used to organize and categorize secrets. They can be applied to multiple secrets and used for filtering.

Tag Properties​

FieldTypeDescription
namestringTag name (unique, alphanumeric with hyphens/underscores)
slugstringAuto-generated URL-friendly identifier (read-only)
descriptionstringOptional description of what this tag represents
colorstringHex color code for UI display (default: #6B7280)
note

Tag names must match the pattern ^[a-zA-Z0-9_-]+$ (letters, numbers, hyphens, underscores only).

List Tags​

GET /api/tags/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Response: 200 OK

[
{
"name": "production",
"slug": "production",
"description": "Production environment resources",
"color": "#EF4444"
},
{
"name": "database",
"slug": "database",
"description": "Database-related secrets",
"color": "#3B82F6"
}
]

Create Tag​

POST /api/tags/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
"name": "production",
"description": "Production environment resources",
"color": "#EF4444"
}

Response: 201 Created

{
"name": "production",
"slug": "production",
"description": "Production environment resources",
"color": "#EF4444"
}

Get Tag​

GET /api/tags/{slug}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Update Tag (PUT)​

PUT /api/tags/{slug}/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
"name": "prod",
"description": "Updated description",
"color": "#DC2626"
}

Partial Update Tag (PATCH)​

PATCH /api/tags/{slug}/
Host: your-tenant-domain.com
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN

{
"color": "#10B981"
}

Delete Tag​

DELETE /api/tags/{slug}/
Host: your-tenant-domain.com
Authorization: Bearer YOUR_ACCESS_TOKEN

Response: 200 OK

{
"message": "Tag deleted successfully",
"detail": "Deleted tag: production"
}
note

Deleting a tag will remove the tag association from all secrets that use it. The secrets themselves will remain unchanged.


Programmatic Access (SDK)​

For backend services or other server-side code, use the SecretService class instead of HTTP API calls.

Import​

from cloud_site.secrets.services import SecretService

Get Single Secret​

# Get a site-level secret
database_url = SecretService.get_secret(site, 'database_url')

# Get an app-level secret (with fallback to site-level)
api_key = SecretService.get_secret(site, 'stripe_api_key', app_slug='billing-app')

# Get secret with default value (no exception if not found)
optional_key = SecretService.get_secret(site, 'optional_feature', default='disabled')

# Disable JSON parsing (returns raw string)
raw_value = SecretService.get_secret(site, 'config', parse_json=False)

Get Secret or None​

# Returns None instead of raising exception if not found
value = SecretService.get_secret_or_none(site, 'optional_key')
if value:
# Use the secret
pass

List All Secrets​

# Get all site-level secrets
secrets = SecretService.list_secrets(site)
# Returns: {'database_url': 'postgresql://...', 'api_key': '...'}

# Get secrets including app-level (with inheritance applied)
secrets = SecretService.list_secrets(site, app_slug='billing-app')

# Include metadata (tags and secret type)
secrets = SecretService.list_secrets(site, include_metadata=True)
# Returns: {
# 'database_url': {
# 'value': 'postgresql://...',
# 'tags': ['database', 'production'],
# 'secret_type': 'database_credentials'
# }
# }

Get Secret History​

from cloud_site.secrets.models import SiteSecret

# Get secret instance
secret = SiteSecret.objects.get(key='api_key', app__isnull=True)

# Get audit history with pagination
history = SecretService.get_secret_history(secret, limit=10, offset=0)

print(f"Total records: {history['total_count']}")
for record in history['results']:
print(f"{record['action']} by {record['user']} at {record['timestamp']}")

Caching​

The SecretService automatically caches secrets using Redis:

  • Private secrets: 5 minutes TTL (configurable via SECRETS_CACHE_TTL)
  • Public secrets: 10 minutes TTL (configurable via SECRETS_PUBLIC_CACHE_TTL)
  • Automatic invalidation: Cache is cleared on create/update/delete

Naming Constraints​

Secret Type Names​

Secret type names must follow this pattern:

^[a-zA-Z0-9_-]+$

Valid examples:

  • database_config
  • jwt-signing-key
  • api_credentials_v2

Invalid examples:

  • database config (spaces not allowed)
  • api.credentials (dots not allowed)
  • config@prod (special characters not allowed)

Tag Names​

Tag names follow the same pattern:

^[a-zA-Z0-9_-]+$

Valid examples:

  • production
  • staging-env
  • critical_alert

Interactive API Documentation​

Explore the full API interactively:

  • Swagger UI: /ninja-docs/ - Interactive API explorer
  • OpenAPI Schema: /ninja-openapi.json - Machine-readable specification

Example URLs​

# Production
https://tenant.yourdomain.com/ninja-docs/

# Development
https://api.taruvi.cloud/ninja-docs/

Additional Resources​