> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formae.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Secrets and generated credentials

> How formae references secrets without storing them, generates credentials it never shows, and rotates them on a schedule.

formae treats a secret as an ordinary managed resource whose value is referenced, never copied. This page covers the three layers of that: referencing a secret's value, having formae generate the value in the first place, and rotating it on a schedule.

<Note>
  The examples below use `local` to bind a resource to a variable so it can be referenced later via `.res`. A `local` still has to be mentioned inside the `forma` block to actually be created. See [Write your first forma](/documentation/get-started/write-your-first-forma) for the full pattern.
</Note>

## Reference, don't store

A resource or target that needs a secret's value references it:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
local dbPasswordSecret: secret.Secret = new {
    label = "db-password"
    name = "app/db-password"
    ...
}

local dbRole: databaserole.DatabaseRole = new {
    label = "db-role"
    roleName = "app"
    password = dbPasswordSecret.res.secretValue
    ...
}
```

What is stored, everywhere, is the reference. The value is read live from the provider at each plugin call, and where formae has to record what it saw, it records a digest, never the cleartext. Nothing shows the value: not the plan, not the inventory, not the logs. Use `.secretValue.at("key")` to pull one entry out of a map-shaped secret and `.json("path")` to reach into a JSON payload. See [Resolvable](/documentation/concepts/resolvable) for the reference mechanism itself.

## Generators: credentials formae draws

Referencing solves reading a credential. A **generator** solves writing one: instead of minting a password at evaluation time and pinning it so it stops changing, declare a generator and bind properties to its output.

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
local dbPasswordGen: formae.PasswordGenerator = new {
    label = "db-password-gen"
    stack = appStack.res
    length = 32
    symbols = false
}

local dbPasswordSecret: secret.Secret = new {
    label = "db-password"
    name = "app/db-password"
    secretString = dbPasswordGen.gen.value
    stack = appStack.res
    target = awsTarget.res
}
```

A generator is a top-level forma entry, like a stack or a target: declare it, then mention it inside `forma { ... }` alongside the resources that bind to it. formae draws the value with a cryptographically secure generator, writes it to every bound property in one command, and keeps only a digest. Without a `rotation` block the generator never rotates on a schedule, though an apply can still redraw it: binding a new destination draws a value for it, and changing the generator's spec so the held generation no longer satisfies it forces a fresh draw. This directly replaces the eval-time pattern:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
// The pattern a generator replaces: minted on EVERY evaluation, so setOnce
// is doing the work of stopping each apply from rotating the credential.
local dbPassword = random.password(32, false)
...
secretString = formae.value(dbPassword).opaque.setOnce
```

Every property bound to a drawing generator must be part of the same apply. The drawn value exists only for the duration of that command, so a destination left out could never be caught up without drawing again; formae refuses the apply and names the destinations it cannot reach.

## Rotation

Give the generator a cadence and formae rotates the credential on schedule, unattended:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
local dbPasswordGen: formae.PasswordGenerator = new {
    label = "db-password-gen"
    stack = appStack.res
    rotation = new formae.RotationSpec { every = 90.d }
}
```

A rotation is one coordinated command that moves everything downstream of the credential: the generator's destinations (the secret), and, transitively, the resources that consume those destinations by reference. In the shape above, rotating the password updates the secret, the database role whose `password` references it, and anything referencing the role, together. Rotation uses ordinary update semantics, so it refuses when the stack, or any of those consumers, has drifted; a stack carrying an [auto-reconcile policy](/documentation/concepts/policies/auto-reconcile) is the standing opt-in to overwrite drift, and there rotation proceeds.

Inspect what rotates with `formae inventory generators`: each generator's cadence, the instant of its last committed rotation, and the resources that take their value from it.

### The window, and what your consumers must do

One thing no rotation scheme with a single credential can remove: there is no transaction spanning the secret store and the system that accepts the credential, so on every rotation there is a brief moment where the two disagree. Anything that reads the secret to authenticate has to be built for that, and the contract is short:

* **Re-read the credential at least once per rotation period.** Resolve it per connection, or cache it with a bounded lifetime. A consumer that reads the credential once at startup breaks on the first rotation and stays broken until it restarts.
* **Tolerate transient authentication failures around a rotation**, for up to your own cache lifetime, by retrying or reconnecting. The failures end on their own once the cache refreshes.

The formae agent is itself a worked example: point `datastore.postgres.passwordSecretArn` at the secret and the agent resolves its own database password per new connection, riding out a rotation of that credential without a restart. See the [configuration reference](/documentation/reference/configuration).

Only overlapping validity, two credentials alternating so one is always accepted, removes the window entirely. formae does not orchestrate that today.

### Choosing a cadence

The schema floor is fifteen minutes, set by what the common secret store sustains rather than by the scheduler: AWS Secrets Manager retains every secret version from the last 24 hours against a fixed quota of 100 versions per secret and advises against sustained writes more often than once every 10 minutes, so a faster cadence exhausts the quota within hours and every rotation after that fails. Treat minutes-scale cadences as something for a drill, and pick a steady-state cadence in days: 30 or 90 days are common choices. A credential that must turn over faster than the floor wants short-lived credentials issued per use, not a scheduler revisiting a long-lived one.

### Key pairs

`KeyPairGenerator` draws an RSA key pair as two named outputs of one draw: `gen.privateKey` (a PKCS#8 PEM) and `gen.publicKey` (a PKIX PEM). Bind each half to its own destination and the two always hold halves of the same pair:

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
local idKey = new formae.KeyPairGenerator {
    label = "identity-key"
    stack = appStack.res
}

// ... one secret binding idKey.gen.privateKey, another binding idKey.gen.publicKey
```

`bits` is 2048 (the default), 3072 or 4096. Both halves are stored opaquely. A binding that names an output the generator's kind does not produce fails the apply with the destination and the output named, so the wrong half can never land silently. Once a key-pair generator is stored, the agent cannot be downgraded to a version that predates the kind.
