# Secret References

> Point a secret at one already managed in a provider's store, by the store's own coordinates

**New in version 0.14**

By default, SecretSpec owns the naming: it stores each secret under its own `{project}/{profile}/{key}` convention. A **secret reference** overrides that for one secret, naming a secret that already exists in the store and is managed outside SecretSpec. SecretSpec then reads (and writes) that existing secret in place, instead of a convention path it controls.

You declare a reference with the `ref` field, a table of provider-independent coordinates:

```toml
[profiles.production]
# The 1Password item "db", its "password" field
DATABASE_URL = { description = "Postgres DSN", ref = { item = "db", field = "password" }, providers = ["prod_vault"] }


# An existing environment variable
GITHUB_TOKEN = { description = "GitHub token", ref = { item = "GITHUB_PAT" }, providers = ["env"] }
```

## Coordinates address a secret from the outside in

A `ref` is not a store-specific address like `op://vault/item/field`. It is a set of provider-independent coordinates, each naming a level of structure that some stores have:

```plaintext
vault                which container holds the item        (1Password only)
└── item             the store's own name for the secret   (always required)
    └── section      a named group of fields               (1Password only)
        └── field    one component inside the item          (structured stores)
            └── version   which revision to read            (supported stores only)
```

Only `item` is universal, because every store names its secrets somehow. `item` is the **complete** name, not a suffix: it replaces the entire convention path, so nothing is prepended.

```toml
# Reads the .env key TOTALLY_DIFFERENT_NAME, not secretspec/myapp/default/DATABASE_URL
DATABASE_URL = { description = "DB", ref = { item = "TOTALLY_DIFFERENT_NAME" }, providers = ["dotenv"] }
```

The other coordinates exist because some stores give a secret internal structure (`field`, `section`), nest it inside a container (`vault`), or keep revisions (`version`). A store that has no equivalent for a coordinate **rejects it with an error naming the coordinate**, rather than silently reading the wrong secret. The [configuration reference](/reference/configuration/#secret-references) documents exactly how each provider maps the coordinates.

## References name, providers route

A `ref` supplies naming only. It does not pin the secret to a particular store. Which provider actually resolves the coordinates follows the ordinary [provider resolution order](/concepts/providers/fallback/): a `--provider` override, then the secret’s `providers` chain, then profile defaults, project `[defaults].providers` in 0.21+, and the user-global default.

This is the difference from pasting a store URL into your config. Because the store is not baked into the reference, the same `ref` works across providers. Each provider in a fallback chain is asked for the same coordinates, and one that cannot interpret them warns and the chain continues:

```toml
[profiles.production]
DATABASE_URL = { description = "Postgres DSN", ref = { item = "db", field = "password" }, providers = ["onepassword://Production", "keyring"] }
```

It also means `--provider` redirects reference secrets exactly like convention secrets, which makes test fixtures trivial: point every reference at a `.env` file without touching the manifest.

```bash
$ secretspec run --provider dotenv:.env.fixtures -- cargo test
```

## Different coordinates per provider

**New in version 0.19**

The original `ref` remains useful when every provider understands one address. When endpoints organize the same logical secret differently, attach a template to each leaf alias and use `refs` only for exceptions:

```toml
[providers]
remote = { uri = "onepassword://Production", ref = { item = "{project}-{profile}", field = "{key}" } }
local = { uri = "dotenv://.env", ref = { item = "{key}" } }
legacy = "onepassword://Legacy"


[profiles.production]
API_KEY = { description = "API key", providers = ["remote", "local"], refs = { legacy = { item = "old-api-item", field = "token" } } }
```

SecretSpec resolves each endpoint independently: `refs.<selected-alias>` wins, then that alias’s template, then convention naming. This applies to primary and fallback reads, writes, and both sides of `import`; the `legacy` ref above can therefore describe an import source without adding it to the normal read route. Templates support `{project}`, `{profile}`, and `{key}` in every coordinate.

Scoped refs deliberately key on aliases, not resolved URIs. Literal provider URIs and bare provider names use convention naming because they have no alias identity. Cached route aliases cannot own a template or scoped ref; configure their individual leaf aliases instead. `refs` and legacy route-wide `ref` are mutually exclusive.

For profile inheritance, `ref` and `refs` (0.19+) are two forms of one address model. A profile that explicitly declares either form replaces the form inherited from `[profiles.default]`: `refs` can replace an inherited `ref`, and `ref` can replace inherited `refs`. A profile entry that declares neither keeps the inherited form. See [Profiles: Switching reference models](/concepts/profiles/#switching-reference-models-019) for an example.

## How it works

* `item` is required; `field`, `vault`, `section`, and `version` are optional and only accepted by stores that have that structure.
* Reads and writes are symmetric: `secretspec set` and interactive `check` write through the coordinates in place wherever the store supports writes. Read-only stores fail with a clear error.
* `ref` and every provider-scoped `refs.<alias>` value are tables. String and URI forms (`ref = "op://vault/item/field"`) are rejected, with an error that spells out the equivalent table.
* Secrets sharing identical coordinates and store are fetched once, and [audit log](/concepts/audit/) events carry the coordinates.

See the [configuration reference](/reference/configuration/#secret-references) for the full specification: the coordinate table, how every provider interprets each coordinate, and the exact rules.

Azure App Configuration (0.20+) native `ref.item` values name one App Configuration key and remain read-only. An App Configuration value can itself be a canonical Azure Key Vault reference; SecretSpec follows that stored URI, including its optional Key Vault version. This is separate from SecretSpec’s `ref.version` coordinate, which Azure Key Vault accepts directly starting in 0.20.