Skip to content

Secret Fields

Overview

Secret fields allow you to mark string properties as sensitive data that should be protected from casual access. When a field is marked as a secret:

  • Masked on read: Values are automatically replaced with ******** in API responses
  • Protected access: Revealing plaintext values requires explicit permission
  • Secure updates: Existing values are preserved when sending the masked placeholder back

Secret fields are ideal for storing: - API keys and tokens - Passwords and credentials - Connection strings - Private configuration values - Any sensitive data that shouldn't be exposed in normal API responses

Creating a Secret Field

Add isSecret: true to any string property definition:

{
  "name": "api_credentials",
  "recordSlug": "api-credentials",
  "properties": [
    {
      "name": "service_name",
      "type": "string",
      "required": true
    },
    {
      "name": "api_key",
      "type": "string",
      "isSecret": true,
      "required": true
    },
    {
      "name": "api_secret",
      "type": "string",
      "isSecret": true
    }
  ]
}

Constraints

  • isSecret is only valid for string type properties
  • Secret fields cannot be marked as isUnique (secrets are not searchable)
  • Secret fields can still have other string constraints like minLength, maxLength, and pattern

Reading Secret Fields

When you retrieve a record with secret fields, the secret values are automatically masked:

# Create a record with secrets
curl -X POST https://api.centrali.io/records/slug/api-credentials \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "data": {
      "service_name": "Payment Gateway",
      "api_key": "pk_live_abc123xyz",
      "api_secret": "sk_live_secret_key_here"
    }
  }'

Response:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "data": {
    "service_name": "Payment Gateway",
    "api_key": "********",
    "api_secret": "********"
  }
}

All secret fields show ******** regardless of the actual value length.

Updating Secret Fields

Preserving Existing Values

When updating a record, you can preserve existing secret values by either:

  1. Omitting the field - The secret remains unchanged
  2. Sending the masked value - "api_key": "********" is detected and ignored
# Update only the service name - secrets are preserved
curl -X PATCH https://api.centrali.io/records/slug/api-credentials/550e8400-... \
  -d '{
    "data": {
      "service_name": "Payment Gateway v2"
    }
  }'

Updating to a New Value

To change a secret value, send the new plaintext value:

curl -X PATCH https://api.centrali.io/records/slug/api-credentials/550e8400-... \
  -d '{
    "data": {
      "api_key": "pk_live_new_key_xyz"
    }
  }'

Clearing a Secret

To remove a secret value, set it to null or an empty string:

curl -X PATCH https://api.centrali.io/records/slug/api-credentials/550e8400-... \
  -d '{
    "data": {
      "api_secret": null
    }
  }'

Revealing Secrets

To access the plaintext value of a secret field, use the reveal endpoint:

curl -X POST https://api.centrali.io/records/slug/api-credentials/550e8400-.../secrets/reveal \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "fields": ["api_key"]
  }'

Response:

{
  "revealed": {
    "api_key": "pk_live_abc123xyz"
  }
}

Reveal All Secrets

Omit the fields parameter to reveal all secret fields:

curl -X POST https://api.centrali.io/records/slug/api-credentials/550e8400-.../secrets/reveal \
  -H "Authorization: Bearer YOUR_TOKEN"

Response:

{
  "revealed": {
    "api_key": "pk_live_abc123xyz",
    "api_secret": "sk_live_secret_key_here"
  }
}

Comparing Secrets

To verify a value matches a stored secret without revealing it:

curl -X POST https://api.centrali.io/records/slug/api-credentials/550e8400-.../secrets/compare \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "field": "api_key",
    "value": "pk_live_abc123xyz"
  }'

Response (match):

{
  "matches": true
}

Response (no match):

{
  "matches": false
}

This is useful for authentication flows where you need to verify a submitted value matches the stored secret without exposing it.

Permissions

Secret operations require specific permissions on the records resource:

Operation Required Permission
Create/Update secrets records:update (standard update permission)
Reveal secrets secrets:reveal
Compare secrets secrets:compare

Workspace administrators automatically have these permissions. For other roles, you'll need to grant them explicitly.

Example: Grant Secret Access to a Role

curl -X POST https://api.centrali.io/permissions \
  -d '{
    "name": "api_integrations_secret_access",
    "resourceName": "records",
    "policyName": "api_integrations_team",
    "actions": ["secrets:reveal", "secrets:compare"]
  }'

Audit Logging

All secret operations are logged for security auditing:

  • Reveal operations: Logs the user, record ID, and which fields were revealed (not the values)
  • Compare operations: Logs the user, record ID, field name, and result (not the compared values)

Secret values are never written to audit logs.

SDK Usage

Node.js SDK

import { Centrali } from '@centrali/sdk';

const client = new Centrali({ apiKey: 'your-api-key' });

// Create a record with secrets
const record = await client.records.create('api-credentials', {
  service_name: 'Payment Gateway',
  api_key: 'pk_live_abc123xyz',
  api_secret: 'sk_live_secret_key_here'
});

// Reading returns masked values
console.log(record.data.api_key); // "********"

// Reveal secrets
const secrets = await client.revealSecrets('api-credentials', record.id, ['api_key']);
console.log(secrets.api_key); // "pk_live_abc123xyz"

// Compare secrets
const matches = await client.compareSecret(
  'api-credentials',
  record.id,
  'api_key',
  'pk_live_abc123xyz'
);
console.log(matches); // true

Compute Functions

Secret fields are also masked in compute function contexts. Use the SDK's revealSecrets() method when you need the plaintext values:

export default async function handler(ctx) {
  const record = await ctx.sdk.records.get('api-credentials', ctx.trigger.recordId);

  // record.data.api_key is "********"

  // Reveal the secret when needed
  const secrets = await ctx.sdk.revealSecrets('api-credentials', record.id);

  // Use the plaintext value
  await callExternalApi(secrets.api_key);
}

Best Practices

  1. Minimize secret exposure: Only reveal secrets when absolutely necessary
  2. Use compare for authentication: Instead of revealing and comparing client-side, use the compare endpoint
  3. Audit secret access: Monitor your audit logs for unexpected reveal operations
  4. Rotate secrets regularly: Update secret values periodically for security
  5. Don't log secrets: Never log plaintext secret values in your code

Security

Encryption at Rest

Secret fields are encrypted using AES-256-GCM before being stored in the database. Each secret value is stored as an encrypted object containing: - The encrypted ciphertext - An encryption flag - Key version information for key rotation support

This means that even if the database is compromised, secret values remain protected.

Key Rotation

Encryption keys can be rotated without downtime. The system supports multiple key versions simultaneously:

  1. Add new key: Update SECRET_KEYS environment variable with a new key version:

    {
      "currentKeyVersion": 2,
      "keys": {
        "1": "old-key-value",
        "2": "new-key-value"
      }
    }
    

  2. New secrets: Automatically encrypted with the new key version

  3. Existing secrets: Still readable (decryption uses stored key version)

  4. Re-encrypt old secrets: Run the rotation job to migrate all secrets to the new key:

# Check rotation status for ALL workspaces
curl -X GET "https://api.centrali.io/admin/secrets/rotation/stats?apiKey=YOUR_ADMIN_API_KEY"

# Check rotation status for a specific workspace
curl -X GET "https://api.centrali.io/admin/secrets/rotation/stats?apiKey=YOUR_ADMIN_API_KEY&workspaceSlug=my-workspace"

# Execute rotation for ALL workspaces (batch mode)
curl -X POST "https://api.centrali.io/admin/secrets/rotation/execute?apiKey=YOUR_ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"batchSize": 100}'

# Execute rotation for a specific workspace only
curl -X POST "https://api.centrali.io/admin/secrets/rotation/execute?apiKey=YOUR_ADMIN_API_KEY&workspaceSlug=my-workspace" \
  -H "Content-Type: application/json" \
  -d '{"batchSize": 100}'

# Rotate secrets for a specific structure only
curl -X POST "https://api.centrali.io/admin/secrets/rotation/structure/api-credentials?apiKey=YOUR_ADMIN_API_KEY&workspaceSlug=my-workspace" \
  -H "Content-Type: application/json" \
  -d '{"batchSize": 100}'

Note: These are app-level admin endpoints that require the ADMIN_API_KEY environment variable. When workspaceSlug is omitted, the endpoints operate on ALL workspaces in batch mode.

  1. Remove old key: After rotation completes, old key can be removed from config

The rotation job re-encrypts: - All record secret fields - All function trigger encrypted parameters

Limitations

  • Secrets are not searchable or filterable
  • Secret fields cannot be used in computed expressions
  • Maximum secret length follows standard string field limits