---
title: Secrets
path: core-concepts/secrets
status: published
---

# 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.
- **Timestamps** — `created_at`, `updated_at`, `expires_at`, `last_accessed_at`.
- **Links** — `rotation_policy_id`, `secret_policy_id`.

Example:

```json
{
  "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:

```
<scope>/<environment>/<service>/<credential>
```

For example:

```
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](./pki). |
| `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
# 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
DELETE /v1/secrets/app/db/password?version=2
```

## Expiration

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

```json
{
  "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
{
  "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
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
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
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

- [Managing Secrets](../api-guides/secrets) — the full CRUD API.
- [Rotation](./rotation) — automated rotation.
- [Policies and Permissions](./policies-and-permissions) — who can read what.
