---
title: Secrets Management
description: Encrypted secret storage with auto-versioning.
---

The `secret` package provides encrypted secret storage with automatic versioning, expiration support, and lifecycle callbacks. Every value written through the service is transparently encrypted via AES-256-GCM before it reaches the store, and decrypted on read.

## Architecture

```
secret.Service
  |-- crypto.Encryptor (AES-256-GCM encrypt/decrypt)
  |-- secret.Store     (pluggable persistence)
  |-- OnAccessFunc     (post-read callback)
  |-- OnMutateFunc     (post-write callback)
```

The `Service` sits between your application code and the backing store. It handles encryption, versioning, and event callbacks so the store implementation only deals with opaque ciphertext.

## Installation

```go
import (
    "github.com/xraph/vault/secret"
    "github.com/xraph/vault/crypto"
)
```

## Creating a Service

```go
enc, err := crypto.NewEncryptor(key) // 32-byte AES-256 key
if err != nil {
    log.Fatal(err)
}

svc := secret.NewService(store, enc,
    secret.WithAppID("myapp"),
    secret.WithOnAccess(func(ctx context.Context, key, appID string) {
        log.Printf("accessed secret %s in app %s", key, appID)
    }),
    secret.WithOnMutate(func(ctx context.Context, action, key, appID string) {
        log.Printf("%s secret %s in app %s", action, key, appID)
    }),
)
```

### ServiceOption

| Option | Signature | Description |
|--------|-----------|-------------|
| `WithAppID` | `WithAppID(appID string) ServiceOption` | Sets the default app ID used when callers pass an empty string |
| `WithOnAccess` | `WithOnAccess(fn OnAccessFunc) ServiceOption` | Registers a callback invoked after `Get` or `GetVersion` |
| `WithOnMutate` | `WithOnMutate(fn OnMutateFunc) ServiceOption` | Registers a callback invoked after `Set` or `Delete` |

### Callback types

```go
// OnAccessFunc is called after a secret is accessed.
type OnAccessFunc func(ctx context.Context, key, appID string)

// OnMutateFunc is called after a secret is set or deleted.
type OnMutateFunc func(ctx context.Context, action, key, appID string)
```

The `action` parameter on mutate callbacks is either `"secret.set"` or `"secret.delete"`.

## Service API

### Get

Retrieves and decrypts a secret by key. Fires the `OnAccess` callback after a successful read.

```go
func (s *Service) Get(ctx context.Context, key, appID string) (*Secret, error)
```

```go
sec, err := svc.Get(ctx, "db-password", "myapp")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(sec.Value)) // plaintext value
```

### GetMeta

Retrieves secret metadata without the value. Useful for listing or inspecting secrets without triggering decryption.

```go
func (s *Service) GetMeta(ctx context.Context, key, appID string) (*Meta, error)
```

```go
meta, err := svc.GetMeta(ctx, "db-password", "myapp")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("version=%d created=%s\n", meta.Version, meta.CreatedAt)
```

### Set

Creates or updates a secret. The value is encrypted before storage and the store auto-increments the version. Fires the `OnMutate` callback with action `"secret.set"`.

```go
func (s *Service) Set(
    ctx context.Context,
    key string,
    value []byte,
    appID string,
    opts ...SetOption,
) (*Meta, error)
```

```go
meta, err := svc.Set(ctx, "db-password", []byte("s3cret!"), "myapp",
    secret.WithMetadata(map[string]string{"env": "production"}),
    secret.WithExpiresAt(time.Now().Add(90*24*time.Hour)),
)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("stored version %d\n", meta.Version)
```

### SetOption

| Option | Signature | Description |
|--------|-----------|-------------|
| `WithMetadata` | `WithMetadata(m map[string]string) SetOption` | Attaches arbitrary key-value metadata to the secret |
| `WithExpiresAt` | `WithExpiresAt(t time.Time) SetOption` | Sets an expiration timestamp on the secret |

### Delete

Removes a secret and all its versions. Fires the `OnMutate` callback with action `"secret.delete"`.

```go
func (s *Service) Delete(ctx context.Context, key, appID string) error
```

```go
err := svc.Delete(ctx, "db-password", "myapp")
```

### List

Returns secret metadata for an app. Values are never returned in list results.

```go
func (s *Service) List(ctx context.Context, appID string, opts ListOpts) ([]*Meta, error)
```

```go
metas, err := svc.List(ctx, "myapp", secret.ListOpts{
    Limit:  50,
    Offset: 0,
})
for _, m := range metas {
    fmt.Printf("%-30s v%d\n", m.Key, m.Version)
}
```

### GetVersion

Retrieves and decrypts a specific version of a secret. Fires the `OnAccess` callback.

```go
func (s *Service) GetVersion(
    ctx context.Context,
    key, appID string,
    version int64,
) (*Secret, error)
```

```go
sec, err := svc.GetVersion(ctx, "db-password", "myapp", 2)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("v%d value: %s\n", sec.Version, string(sec.Value))
```

### ListVersions

Returns all versions of a secret (without decrypted values).

```go
func (s *Service) ListVersions(ctx context.Context, key, appID string) ([]*Version, error)
```

```go
versions, err := svc.ListVersions(ctx, "db-password", "myapp")
for _, v := range versions {
    fmt.Printf("v%d created=%s by=%s\n", v.Version, v.CreatedAt, v.CreatedBy)
}
```

## Entity types

### Secret

The full secret entity, including both encrypted and decrypted values. The `Value` field is populated only after decryption by the service; it is never serialized.

```go
type Secret struct {
    vault.Entity
    ID              id.ID             `json:"id"`
    Key             string            `json:"key"`
    Value           []byte            `json:"-"`  // decrypted value -- never serialized
    EncryptedValue  []byte            `json:"-"`  // encrypted at rest
    Version         int64             `json:"version"`
    EncryptionAlg   string            `json:"encryption_alg"`
    EncryptionKeyID string            `json:"encryption_key_id"`
    ExpiresAt       *time.Time        `json:"expires_at,omitempty"`
    AppID           string            `json:"app_id"`
    Metadata        map[string]string `json:"metadata,omitempty"`
}
```

| Field | Type | Description |
|-------|------|-------------|
| `ID` | `id.ID` | Unique secret identifier (TypeID) |
| `Key` | `string` | Lookup key for the secret |
| `Value` | `[]byte` | Decrypted plaintext (never serialized to JSON) |
| `EncryptedValue` | `[]byte` | Ciphertext stored at rest (never serialized to JSON) |
| `Version` | `int64` | Auto-incremented version number |
| `EncryptionAlg` | `string` | Algorithm label, set to `"AES-256-GCM"` when encrypted |
| `EncryptionKeyID` | `string` | Identifier of the encryption key used |
| `ExpiresAt` | `*time.Time` | Optional expiration timestamp |
| `AppID` | `string` | Application scope identifier |
| `Metadata` | `map[string]string` | Arbitrary key-value metadata |
| `CreatedAt` | `time.Time` | Timestamp from embedded `vault.Entity` |
| `UpdatedAt` | `time.Time` | Timestamp from embedded `vault.Entity` |

### Meta

Public metadata for a secret. Never includes the value. Returned by `Set`, `GetMeta`, and `List`.

```go
type Meta struct {
    ID        id.ID             `json:"id"`
    Key       string            `json:"key"`
    Version   int64             `json:"version"`
    ExpiresAt *time.Time        `json:"expires_at,omitempty"`
    AppID     string            `json:"app_id"`
    Metadata  map[string]string `json:"metadata,omitempty"`
    CreatedAt time.Time         `json:"created_at"`
    UpdatedAt time.Time         `json:"updated_at"`
}
```

### Version

A historical version record for a secret.

```go
type Version struct {
    ID             id.ID     `json:"id"`
    SecretKey      string    `json:"secret_key"`
    AppID          string    `json:"app_id"`
    Version        int64     `json:"version"`
    EncryptedValue []byte    `json:"-"`
    CreatedBy      string    `json:"created_by"`
    CreatedAt      time.Time `json:"created_at"`
}
```

### ListOpts

```go
type ListOpts struct {
    Limit  int
    Offset int
    AppID  string
}
```

## Store interface

The store handles persistence. Vault ships with in-memory and PostgreSQL implementations (see [Stores](/docs/vault/stores/postgres)).

```go
type Store interface {
    GetSecret(ctx context.Context, key, appID string) (*Secret, error)
    SetSecret(ctx context.Context, s *Secret) error
    DeleteSecret(ctx context.Context, key, appID string) error
    ListSecrets(ctx context.Context, appID string, opts ListOpts) ([]*Meta, error)
    GetSecretVersion(ctx context.Context, key, appID string, version int64) (*Secret, error)
    ListSecretVersions(ctx context.Context, key, appID string) ([]*Version, error)
}
```

The store is responsible for auto-incrementing the `Version` field on `SetSecret`. When a key already exists, a new version is created and the previous version is preserved.

## Encryption behavior

When an `Encryptor` is provided to `NewService`:

1. **On write** (`Set`): The plaintext value is encrypted via `Encryptor.Encrypt`, producing a `nonce || ciphertext` byte slice. The `EncryptionAlg` field is set to `"AES-256-GCM"`.
2. **On read** (`Get`, `GetVersion`): The stored `EncryptedValue` is decrypted via `Encryptor.Decrypt` and the plaintext is placed in `Secret.Value`.

When no encryptor is provided, the raw value bytes are stored directly in `EncryptedValue` as a fallback. This is suitable for development but not recommended for production.

## Versioning behavior

Every call to `Set` creates a new version of the secret:

1. The store creates a new version entry with the current value.
2. The `Version` field on the secret is incremented.
3. Previous versions remain accessible via `GetVersion` and `ListVersions`.
4. `Delete` removes the secret and **all** its versions.

## Full example

```go
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/xraph/vault/crypto"
    "github.com/xraph/vault/secret"
)

func main() {
    ctx := context.Background()

    // Create encryptor with a 32-byte key.
    key := make([]byte, 32) // In production, load from a key provider
    enc, err := crypto.NewEncryptor(key)
    if err != nil {
        log.Fatal(err)
    }

    // Create the service with an in-memory store.
    svc := secret.NewService(memStore, enc,
        secret.WithAppID("myapp"),
        secret.WithOnAccess(func(ctx context.Context, key, appID string) {
            fmt.Printf("[audit] accessed %s\n", key)
        }),
    )

    // Store a secret.
    meta, err := svc.Set(ctx, "api-key", []byte("sk-live-abc123"), "",
        secret.WithMetadata(map[string]string{"provider": "stripe"}),
        secret.WithExpiresAt(time.Now().Add(365*24*time.Hour)),
    )
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("stored: version=%d\n", meta.Version)

    // Retrieve it.
    sec, err := svc.Get(ctx, "api-key", "")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("value: %s\n", string(sec.Value))

    // Update it (creates version 2).
    meta, err = svc.Set(ctx, "api-key", []byte("sk-live-xyz789"), "")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("updated: version=%d\n", meta.Version)

    // List all versions.
    versions, err := svc.ListVersions(ctx, "api-key", "")
    if err != nil {
        log.Fatal(err)
    }
    for _, v := range versions {
        fmt.Printf("  v%d created=%s\n", v.Version, v.CreatedAt)
    }
}
```
