# Fused

> The only integration gateway you need

Publication: https://usefused.com/blog/

---

# We used to make you send your API keys with every request

Canonical: https://usefused.com/blog/post/buckets-one-place-for-secrets
Readable HTML: https://usefused.com/blog/read/buckets-one-place-for-secrets
Markdown: https://usefused.com/blog/posts/buckets-one-place-for-secrets.md
Published: 2026-10-25T21:43:28.087Z
Updated: 2026-11-01T02:28:13.983Z
Tags: Buckets, Security, Integration

> The first version of Fused never stored a credential. Here's why we added buckets, and what changes for your team, your agents and your users when keys live in one place.

The first version of Fused didn't store credentials at all. You sent your provider key along with each request, the Engine used it for that one call, and then it was gone. We liked that. If we never hold a key, we can't leak one.

It held up until people started building real things with it. Every service that called Fused needed its own copy of the key, so the key still ended up in every `.env` file and CI pipeline, which is the mess Fused was supposed to clean up. Agents had it worse. The only way to let one call a provider was to put the key somewhere the model could read it.

So we added buckets. You can still pass a key on a single request when you need to, but most teams have stopped.

A bucket is a named store in your workspace. It holds provider API keys, OAuth client secrets, certificates, a few plain config values, and the OAuth connections your users make. Apps point at a bucket by name. When a request leaves Fused for the provider, the Engine looks up the right credential in that bucket and attaches it. Your code, your CI and your agents only ever hold Fused tokens.

Here's what that looks like over the life of one key.

## Putting a key in

```bash
fused-cli connect resend --bucket prod
```

The CLI lists the ways the service can authenticate and marks each one as configured, incomplete or expired. You type the value into a masked prompt, and it's live as soon as you press enter. In CI you pass it as one quoted argument with `--no-input` instead.

Each secret is encrypted with its own data key, and that data key is encrypted with the Engine's master key. Listing a bucket shows you names and metadata. The values stay where they are.

An app names its bucket in its config, and any one service in it can use a different bucket:

```yaml
apiVersion: fused/v1
kind: sdk
name: support-portal
version: "1.0.0"
language: typescript
bucket: prod
services:
  resend:
    version: "<enabled-service-version>"
    operations: [postEmails]
  slack:
    version: "<enabled-service-version>"
    operations: [chatPostMessage]
    bucket: slack-credentials
```

The [store credentials guide](https://usefused.com/docs/bucket/store-credentials) covers every credential type, plus certificates and plain values.

## Deciding who can touch it

Using a key and seeing a key are separate permissions in Fused, and we kept them apart on purpose.

A team with `use` on a bucket can build and ship apps that depend on it. A team with `manage` can also add and replace credentials and look after user connections. Neither can read a value back. That needs a separate reveal permission, the secret has to have been marked exportable first, and every reveal goes in the audit log. Some kinds of credential, like OAuth client secrets and mTLS keys, can't be marked exportable at all.

```bash
fused-cli workspace team access bucket grant payments prod use
```

The usual setup is one platform team that owns the keys while everyone else builds on them. When someone leaves, there's no copy of the key on their laptop to worry about, because it was never there. More in [Share a bucket](https://usefused.com/docs/bucket/share-a-bucket).

## Changing it

To rotate the key, run the same `connect` command with the new value. The Engine reads credentials as each request goes out, and saving a new one clears the cached copy on that Engine and on every other replica. The next call from any app on the bucket uses the new key, and nothing needs redeploying.

If you know when a key expires, record it with `--expires-at`. After that date the CLI shows it as expired and the Engine stops sending it. Calls fail with an error naming the key and its expiry date, which is quicker to fix than working backwards from a 401.

## Letting an agent in without giving it the key

An agent connects to a Fused MCP server with OAuth, where you tick the actions and pick the account on a consent screen, or with an execution token. Either way it holds a Fused credential and never the provider's. Resend and Jira have never seen it, so it's no use anywhere else. The Engine adds the real credential as each call goes out, and the agent only sees what comes back. The connection is limited to the actions you approve and tied to one person's account, a token can be set to expire as well, and revoking it leaves the provider key alone. [Letting an agent act as you, and only as you](https://usefused.com/blog/post/agent-tokens-act-on-your-behalf) walks through it.

## When the key belongs to your user

A lot of products act for their users: opening a pull request in the user's repo, or filing an issue in their Jira. Now there's a credential per user, and this is usually where a team ends up writing its own OAuth handling.

In Fused you pick a stable reference for each user, usually your own user ID, and start a connection:

```bash
fused-cli service connect jira \
  --bucket prod \
  --user-ref user_123 \
  --scope read:jira-work
```

Your app can start the same flow from its SDK. Fused runs the provider's sign-in, including any extra fields the provider needs such as a Jira site name, and your SDK only gets back a link to send the user to. The user's tokens are encrypted into the bucket and aren't returned to your app. From then on you call operations with the same reference as `endUserRef`.

Refreshing is Fused's job. It refreshes tokens when the Engine starts and every hour after that, and refreshes one just before use if it's about to expire. Only one refresh runs for a token at a time. A connection is marked `reconnect_required` only when refreshing has genuinely failed, for example because the user revoked access.

One user's token sometimes covers several Jira sites. If there's only one, Fused uses it. If there are several and no default, the call returns an error rather than guessing which site you meant. To see who's connected and whether each token is healthy, run `fused-cli bucket connections prod --service jira`.

You might not need to register an OAuth app at all. For services where Fused runs a [managed app](https://usefused.com/docs/workspace/use-managed-service), you point the service at `${fused.bucket.auth.<service>.<auth-name>}`. Fused keeps that app's client secret, and your users' tokens still sit in your bucket. The [connect a user guide](https://usefused.com/docs/bucket/connect-user-accounts) walks through both routes.

## Several products, one sign-in

Connections are stored against the bucket, not against the app that created them. Say your company runs a customer app, an internal admin tool and a support agent, all on one bucket. A user who connects Jira through the customer app can then be acted for by the admin tool and the agent too, using the same reference. Nobody signs in three times.

OAuth apps can be shared in the same way. Google uses one OAuth client across Gmail, Sheets and Drive, so you register it once on one service and point the others at it:

```yaml
services:
  google-sheets:
    version: v4
    operations: [spreadsheets.get]
    auth:
      type: oauth
      name: oauth2
      ref: "${bucket.auth.gmail.oauth2}"
```

Gmail only has to be enabled in the workspace; the app doesn't need any of its operations. Rotate that client secret and every service pointing at it picks up the change. Users still approve each service separately, because each one keeps its own tokens.

Inside a [Unified App](https://usefused.com/blog/post/unified-apps-one-place), one run can act as the same person in several services:

```typescript
const user = fused.forUserRef(input.userRef);
const issue = await user.jira.getIssue({ issueIdOrKey: input.issueKey });
```

If that person has a different reference in each service, `fused.forServiceUserRefs({ jira: "...", github: "..." })` maps them one by one, and throws if the app calls a service you didn't map.

## A few limits

Buckets are for the credentials Fused uses when it calls a provider. They aren't a vault for your own app's database password, and execution tokens can't read anything back out of them.

References between services only work inside one bucket. Fused rejects a cross-bucket reference when you plan the app, which is what keeps a staging app from quietly borrowing a production client secret. Give each real boundary its own bucket, such as `prod` and `staging`. Fused won't delete a bucket while an app still uses it.

