Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Secrets

The secret is the primary object in ScaiVault. Everything else — policies, rotation, audit — exists in service of reading and writing secrets safely.

Shape#

A secret has:

  • Path — slash-separated address, e.g. environments/production/salesforce/api-credentials. Tenant-scoped by default.
  • Secret type — how the value should be interpreted. One of kv, json, certificate, ssh_key, api_key.
  • Data — the actual payload, a JSON object with arbitrary fields. {"password": "..."}, {"client_id": "...", "client_secret": "..."}, {"key": "sk_live_..."} — anything.
  • Version — an integer. Each write creates a new version; the old versions stay readable until aged out.
  • Metadata — description, tags, owner, custom key-values. Not secret; searchable.
  • Timestampscreated_at, updated_at, expires_at, last_accessed_at.
  • Linksrotation_policy_id, secret_policy_id.

Example:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "path": "environments/production/salesforce/api-credentials",
  "secret_type": "json",
  "version": 3,
  "data": {
    "client_id": "3MVG9...",
    "client_secret": "REDACTED"
  },
  "metadata": {
    "description": "Salesforce OAuth credentials",
    "tags": ["salesforce", "oauth", "production"],
    "owner": "team:integrations"
  },
  "created_at": "2026-01-15T10:30:00Z",
  "updated_at": "2026-04-20T14:22:00Z",
  "expires_at": null,
  "rotation_policy_id": "rot_quarterly"
}

Paths#

Paths are /-separated. They have meaning to you — the service at environments/production/salesforce/ probably knows what secrets live under that prefix. ScaiVault just matches strings.

Rules:

  • Maximum depth: 10 levels.
  • Maximum length: 512 characters total.
  • Allowed characters: a-z, 0-9, -, _, /.
  • Case-sensitive.
  • No leading/trailing slashes.
  • No empty segments (//).

Paths encouraged, not required, to follow a pattern:

text
1
<scope>/<environment>/<service>/<credential>

For example:

text
1
2
3
4
environments/production/salesforce/api-credentials
environments/staging/postgres/readonly
shared/webhook-signing-keys/scaivault
tenants/acme-corp/billing/stripe-key

This is convention, not enforcement. ScaiVault doesn't care about your path scheme as long as it's consistent — your policy patterns will match what you write.

Secret types#

The secret_type field is a hint to consumers and to the UI. It doesn't change storage behavior.

Type Typical shape Use when
kv {"key": "value"} Single credentials, simple config
json Arbitrary nested JSON Multi-field credentials, OAuth clients, config blobs
certificate {"cert_pem": "...", "key_pem": "...", "chain_pem": "..."} TLS certificates stored by hand. For ScaiVault-issued certs, use the PKI API.
ssh_key {"public_key": "...", "private_key": "..."} SSH host or user keys
api_key {"key": "sk_live_..."} Single-value API tokens

Rendering, masking, and search filters in the admin UI key off the type. Your code can ignore it if all you need is .data.

Versioning#

Every write creates a new version. The old versions stay readable and count against the secret's max_versions setting (default: 10).

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# v1: initial write
PUT /v1/secrets/app/db/password -d '{"data": {"password": "v1-value"}}'

# v2: update
PUT /v1/secrets/app/db/password -d '{"data": {"password": "v2-value"}}'

# Read current (v2)
GET /v1/secrets/app/db/password
# → {"version": 2, "data": {"password": "v2-value"}}

# Read v1 explicitly
GET /v1/secrets/app/db/password?version=1
# → {"version": 1, "data": {"password": "v1-value"}}

# List versions
GET /v1/secrets/app/db/password/versions
# → [{"version": 2, "created_at": ..., "is_current": true}, {"version": 1, ...}]

When max_versions is exceeded, ScaiVault deletes the oldest versions first (keeping the current always). Set max_versions higher if you need longer history for compliance.

Why not overwrite?#

Two reasons:

  1. Grace during rotation. A client that read v2 thirty seconds before rotation can still fetch v2 explicitly if the caller is allowed. Gives long-running jobs a window to finish.
  2. Forensics. "What was the value of this secret on Monday?" is answerable.

You can delete a specific version if it contained something that shouldn't have been stored:

bash
1
DELETE /v1/secrets/app/db/password?version=2

Expiration#

Set expires_at (absolute) or expires_in (duration) on write:

json
1
2
3
4
5
6
{
  "data": {"key": "..."},
  "options": {
    "expires_in": "90d"
  }
}

Expired secrets return 410 secret_expired on read. Depending on configuration, they can be auto-deleted or kept in a read-only state.

Use expiration for secrets that genuinely have a known lifetime (short-term test credentials, one-off access tokens). For recurring rotation, use a rotation policy instead.

Metadata#

Metadata is searchable and non-secret:

json
1
2
3
4
5
6
7
8
9
{
  "metadata": {
    "description": "Salesforce OAuth credentials",
    "tags": ["salesforce", "oauth", "production"],
    "owner": "team:integrations",
    "environment": "production",
    "compliance": "pci"
  }
}

List endpoints accept tag= filters. Custom fields are indexed for prefix search.

Updating metadata does not create a new version:

bash
1
PATCH /v1/secrets/app/db/password -d '{"metadata": {"tags": ["critical"]}}'

Deletion#

Two flavors:

Soft delete (default): the secret is marked deleted and unreadable, but stays in the database for a recoverable retention period (30 days by default).

bash
1
DELETE /v1/secrets/app/db/password

Response includes recoverable_until. A soft-deleted secret can be restored with POST /v1/secrets/{path}/restore.

Hard delete (requires admin): removes the row permanently.

bash
1
DELETE /v1/secrets/app/db/password?permanent=true

Hard-deleted paths can be reused. Soft-deleted paths cannot — if you try to write to a soft-deleted path, you get 409 secret_exists. Restore or wait out the retention, or use a different path.

What's next#

Updated 2026-05-17 13:26:50 View source (.md) rev 2