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

graph TD
A[Site Secrets] -->|inherited by| B[App Secrets]
B -->|resolves to| C[Secret Value]

D[Secret Type] -->|validates| A
D -->|validates| B

E[Tags] -->|categorize| A
E[Tags] -->|categorize| B

F[Type Classification] -->|system/custom| D

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

Django 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

{
"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, Celery tasks, 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
http://localhost:8000/ninja-docs/

Additional Resources