Secrets, Service Setup & Materializer

Trinity stores every API key, token, and config file your project needs in one encrypted store. The full management surface lives in Project Settings → Secrets, which splits into three sub-tabs:

  • Overview — a glance card with counts (configured / needs setup), a pending-setup list per service, and a "Re-sync files" button for materializable rows.
  • Shared — package-bound secrets every member of the workspace can see (tier 3/4). Table view, filters, bulk .env import, per-package editing.
  • Yours — your personal overrides for this project (tier 1/2). Same editor, scoped to your account; no other member sees these rows.

End-to-end encryption

Every secret is encrypted and decrypted on your own devices — the server stores and serves only ciphertext. There is no server-side key: nobody without one of your enrolled devices can read your values, including Trinity.

Two keys decide who can decrypt a row, and they are deliberately different things — one is yours alone, the other is the group's:

  • Your member key is per-person. It encrypts everything scoped to you — your Yours rows and your own workspace-wide rows. Nobody else in the workspace can decrypt them, even though the rows sit in the same shared database.
  • The workspace key is the group key. It encrypts the shared rows (project-shared rows and workspace defaults). Only the devices of current members hold it, and removing a member rotates it — see Workspaces.

A device only holds keys once it's been approved. Rows your device can't decrypt show as locked rather than exposing anything. Manage devices, approvals, and your recovery code in App Settings → Profile → Devices.

Where secrets land on disk

Most secrets are environment variables — they get injected into the agent's process at execution time and never touch your filesystem. A handful (like dexie-cloud.json or .env.local) need to live as files inside your project — Trinity writes those into your repo and lists each one in git's per-machine ignore file so they never get committed. See The materializer below.

The Shared and Yours editors

Open Project Settings → Secrets and pick Shared or Yours. You'll see a table grouped by key name. A key with two target overrides shows up as one row that expands to reveal the per-target values.

What each row tells you

Column Meaning
Key name The env var name (e.g. STRIPE_SECRET_KEY)
Package Which package (codebase directory) this row binds to. Every secret binds to one package.
Service The service that owns this key (e.g. stripe, dexie-cloud)
Purpose runtime (env var), provider (AI provider key), file (materializes to disk)
Value Masked. Click the eye icon to reveal — your device decrypts it locally and shows briefly.
Updated Last write time

Filters

  • Search — substring match on key name
  • Targets — filter by "All targets" or check one or more individual packages (each shown by the target label(s) it ships).
  • Purpose — narrow to runtime, provider, or file-purpose secrets

Adding a secret

Click Add secret. The dialog asks for:

  • Key name — the env var name (e.g. STRIPE_SECRET_KEY)

  • Value — what you're storing. Sensitive by default; the input masks the value as you type.

  • Targets — required. Pick a specific package (shown by its target label), or choose "All targets" to fan the value out to every package (one row per package). One key can fan out to multiple packages in a single submit.

  • Purpose — defaults to runtime. Pick provider for AI provider keys, file for config files (file purpose adds a destinations field — see The materializer).

  • Sensitive — UI dimension only; controls whether the value is masked by default. Every row is encrypted at rest regardless.

  • Registry publish setting — turn this on for what a release uses to publish the package to its registry. NPM_TOKEN (an npm package) or CARGO_REGISTRY_TOKEN (a Rust crate) is the token. NPM_REGISTRY is the registry an npm package's token publishes to, as a URL such as https://npm.example.com/; leave it unset to publish to npm's public registry. A crate always publishes to crates.io. The name must be one of those. A registry setting stays out of the package's .env, so your app and your agents never see it; only the release's publish step reads it. The list marks a token Registry token and a registry Publish registry. A key name already stored the other way on a package (an ordinary NPM_TOKEN your install needs, say) has to be deleted before it can be stored as a registry setting there.

    The token only ever goes to that registry. A package whose package.json names a different registry in publishConfig doesn't publish: the release stops and names both registries, so you either store that registry as NPM_REGISTRY or remove it from the manifest.

Trinity infers sensitive from the key name (*_SECRET, *_KEY, *_TOKEN, *_PASSWORD default to true) when you don't override.

No packages yet? Because every secret binds to a package, a project with no packages has nothing to add a secret to. Instead of an add form, the page shows guidance: "No packages yet — project secrets are bound to a package, and your packages are set up during architect planning. Finish planning and you can add secrets here." A Continue in Architect button goes there, switching to the project whose secrets you were looking at on the way, so Architect opens that project rather than whichever one was active. Once the project has at least one package, the add form appears.

Editing a secret

Click any row to open the edit dialog. You can change the value, move the binding to a different package, update the label, or delete the entry.

Secrets resolve per package: if you have your own Yours value for DATABASE_URL on a package and there's also a shared value for the same key on that package, your personal value wins for your runs. Two things that ship from one codebase directory read the same .env, because there is only one file there.

Permissions

Who can edit, reveal, or delete a secret depends on the row's tier and the workspace's Manage Secrets permission setting.

Tier Edit / reveal / delete
Project-shared (Shared) Owner / manager always. Members pass when the workspace's Manage Secrets permission is set to "All Members", blocked when set to "Owner Only".
Project per-member (Yours) Each member only. Pinned to your account — nobody else sees, edits, or reveals your Yours rows.
Workspace default (workspace-wide) Same Manage Secrets setting as project-shared rows, read at the workspace level: owner / manager always, members too when it is set to "All Members". Every member can read masked values and use them at runtime whatever it is set to.
Your workspace-wide row (/me) Each member only. Same isolation as project per-member — nobody else can see the rows your member key encrypts.

Configure the Manage Secrets permission in Workspace Settings → Access Control → Permissions, with optional per-project overrides in Project Settings → General → Permissions. The default is "owner only", and the gate runs in every workspace — in one that is just you, you pass it as its owner. A per-project override governs that project's own secrets; the workspace-wide rows read the workspace setting, since they belong to no project.

Bulk .env import

Click Import .env to paste a .env file. Trinity:

  1. Parses the file (handles quoted values, escaped newlines, inline comments)
  2. Runs the sensitivity heuristic per key
  3. Lets you pick which package each key binds to ("All targets" fans the value out to every package — one row each)
  4. Picks a conflict policy: keep existing, overwrite, or skip
  5. Submits everything in one transactional write — the whole batch rolls back on any failure

Per-row conflict overrides are available if you want different behavior for some keys. Result rows report created | updated | skipped | error so you can see exactly what happened.

Provider keys

The Project Settings → AI Models → Providers card is a focused view of purpose='provider' rows for AI providers (Anthropic, OpenAI, DeepSeek, Moonshot, Z.ai, Qwen, Xiaomi, xAI, Ollama, Sakana). Same encrypted store underneath; the card just narrows the table to provider rows.

You can also set provider keys for the whole workspace, across every project in it, from the Provider Configuration card in Workspace Settings → Mine → AI Models → Providers. Cascade rule: a project-scoped key wins over a workspace-wide one.

The materializer

Some setup flows produce config files Trinity needs to write to disk — dexie-cloud.json, .env.local, certificates, or project-specific YAML. These get stored as purpose='file' secrets with destinations (where on disk to write them):

  • package — inside the consuming app's directory (e.g. apps/web/.env.local for a turborepo package)
  • repo — at the repo root (e.g. dexie-cloud.json next to package.json)
  • trunk — at your workspace root (project-wide config)

Trinity's materializer is what writes these files. It runs:

  • Right after a worktree is created (so every parallel story sees the right files)
  • After the setup runner finishes capturing values from a CLI
  • After successful story commits (so freshly-written secrets land before the next phase reads them)
  • On demand via Project Settings → Re-sync files

Keeping these files out of git

Trinity never commits a file it writes for you. Each materialized file, the .env Trinity composes for each codebase, and the local Docker state it keeps under a codebase's .trinity/local/ folder are listed in a Trinity block inside your repo's .git/info/exclude — git's own per-machine ignore file, which is never committed:

# === Trinity-managed (do not edit between markers) ===
**/.trinity/local/
.env
/apps/web/.env.local
/config/dexie-cloud.json
# === /Trinity-managed ===

.env and .trinity/local/ are always listed, even in a project with no secrets. Nothing broader is, so the documentation Trinity keeps in each codebase's .trinity/docs/ is committed like any other file.

Trinity leaves your .gitignore to you. The one change it makes there is removing a Trinity block that ignores .trinity/, and only in a repo a story works in, as part of that story's pull request. Don't edit between the markers in the exclude file — Trinity rewrites the block. If you delete a file-purpose secret, Trinity removes the file and its line on the next materializer pass.

How agents see your secrets

When a story executes, Trinity assembles the env var bag from:

  1. Package-specific rows for the package the story's target ships from
  2. Global rows (across all your projects)

The first hit wins per key name. Multi-package stories that don't have a clear package binding raise a gate so you can split the secret per-package.

For file-purpose secrets, agents just see the file on disk — the materializer wrote it before the agent ran.

Service setup gates

If you start a story that needs a service Trinity sees as unconfigured, the story pauses at a service setup required gate. The gate dialog has the setup runner pre-bound to the right target — finish the flow and the story resumes automatically. Agents can also raise this gate mid-execution via the signal_service_required tool when they hit a missing service that wasn't on Trinity's radar.

See Execution Gates for the full gate catalogue.