Keysmith
1.x
Docs/Keysmith/Key Rotation
Open

Reading2 min
Updated31 Jul 2026
Sourcev1/subsystems/rotation.mdx

The rotation subsystem enables zero-downtime API key rotation. When a key is rotated, both the old and new keys validate during a configurable grace period, giving clients time to migrate.

Rotating a key01

import "github.com/xraph/keysmith/rotation"

newResult, err := eng.RotateKey(ctx,
    keyID,
    rotation.ReasonScheduled,
    24*time.Hour, // grace period
)
if err != nil {
    log.Fatal(err)
}

fmt.Println("New Key:", newResult.RawKey) // save this — shown once

Rotation reasons02

ReasonConstantWhen to use
scheduledrotation.ReasonScheduledRegular rotation schedule
compromisedrotation.ReasonCompromisedKey may have been leaked
expiringrotation.ReasonExpiringKey approaching expiration
manualrotation.ReasonManualManual rotation by admin

Grace period behavior03

During the grace period:

  1. The new key validates normally (state: active)

  2. The old key still validates (state: rotated, within grace window)

  3. After grace expiry, the old key stops validating

Time ─────────────────────────────────────────────►
     │ Rotation    │ Grace period      │ Grace expired
     │             │                   │
Old: │ active      │ rotated (valid)   │ rotated (invalid)
New: │ active      │ active            │ active

Viewing rotation history04

records, err := eng.ListRotations(ctx, keyID, &rotation.ListFilter{
    Limit: 10,
})

for _, rec := range records {
    fmt.Printf("Rotated at %s, reason: %s, grace: %s\n",
        rec.CreatedAt, rec.Reason, rec.GraceTTL)
}

Rotation record fields05

FieldTypeDescription
IDid.RotationIDUnique record identifier
KeyIDid.KeyIDRotated key
OldHashstringPrevious key hash
NewHashstringNew key hash
ReasonReasonWhy the rotation happened
GraceTTLtime.DurationGrace period duration
GraceExpirytime.TimeWhen the grace period ends

Rotation store interface06

type Store interface {
    Create(ctx context.Context, r *Record) error
    GetByID(ctx context.Context, id id.RotationID) (*Record, error)
    ListByKeyID(ctx context.Context, keyID id.KeyID, filter *ListFilter) ([]*Record, error)
    GetActiveGrace(ctx context.Context, oldHash string) (*Record, error)
    DeleteByKeyID(ctx context.Context, keyID id.KeyID) error
}