Docs

Connect a service once, keep the secret in one vault

RepoOps keeps a team's service credentials in one encrypted store on the hosted deployment. Every member sees a mask and a status, never the secret. An owner or admin connects, tests and disconnects; a machine bound by one of them pulls four kinds of credential down; an OAuth grant never leaves the server.

For: the owner or admin who connects a team's services, and the engineer whose machine reads them

What it does, and why it helps

A connector is one row in a team-scoped table: a provider, a scope, a non-secret identity, and a credential encrypted with a key of its own before it reaches the column. Three families share the table. Inbound sources RepoOps reads (Vercel, Sentry, PostHog, Postgres, Neon, Anthropic, GitHub, Cloudflare, a Vercel log drain), the customer financial connectors behind the Financial Model (Xero, Ramp, Stripe, connected by OAuth only), and outbound targets the integration bus posts a redaction-clean security event to (Splunk, Microsoft Sentinel, Google Chronicle, Jira, Linear, GitHub Issues, Asana, Microsoft Teams, a CI/CD gate). No read returns the credential. The identity is plaintext and shown to every member, so a credential-shaped value typed into an identity field is refused, and the operator is told to rotate it.

What reaches a machine is narrower than what the vault holds. A machine bound by an owner or admin pulls the Vercel, Sentry, PostHog and Anthropic credentials once an hour, on the hour, and writes each into its data-dir .env under the name the local adapter already reads. A machine bound by a member pulls nothing. An OAuth envelope is refused at the pull route, so a refresh token never leaves the server. Postgres stores the path of a slow-query log and no secret. The outbound targets are dispatched by an hourly sweep on the hosted side and never reach a machine. The whole vault is dormant until the operator sets its key, and it fails closed rather than store plaintext.

The pain. The same Vercel token sits in five data-dir .env files. Nobody knows who holds a copy, a rotation means five edits, and the token that reaches a SIEM is a paste in a form somebody has since left.

The point of view. A shared secret should have one home, one encryption key that is not shared with anything else, and a record of every read. Who may use it is a scope the owner chooses on the row, not a side effect of who once had the file.

What gets easier. Connecting once. The card shows the provider, the team's own label, Team or Company, the status, the identity and the mask, and what the last test did: whether the provider answered or only the credential decrypted.

When it helps. A team on the hosted tier with more than one machine reading the same service, a team that wants security events in a ticket tracker or SIEM, or a team connecting its own books to the Financial Model without handing RepoOps a pasted key.

Its limits. It is hosted only; the desktop app carries a pointer to it. Only four credentials sync to a machine, and only to a machine bound by an owner or admin. Company scope reaches sibling teams in the same company only for rows connected after the reach column landed. An OAuth provider needs a vendor app registered by the operator, or its Connect button does not appear. There is no retention window and no rollback of a re-authenticated credential.

Understand it in 30 seconds

30.1 s, captions on. Narration: Microsoft Zira Desktop (provisional voice; an approved narration source is pending).Transcript
Read the narration
  1. 0:00 The same token sits in five env files.
  2. 0:03 Nobody knows who holds it.
  3. 0:06 RepoOps keeps it in one encrypted vault.
  4. 0:09 Reach is a choice the owner makes.
  5. 0:13 Every member sees the mask and the status, never the secret.
  6. 0:17 A test says whether the provider answered, or only that it decrypts.
  7. 0:23 Machines pull it; every pull is audited.
  8. 0:26 The guide covers scope and reach.

Synthetic example. Read the guide

Where to find it

  • Hosted: repoops.ai/team/connectors, from Connections in the sidebar, under Workspace tools.
  • Desktop: hosted only.

When to use it

One Vercel token for every machine on the team

Situation. Three engineers each pasted the production Vercel token into their own data-dir .env. One left. The vault key is set on the deployment.

What you do. On the Connectors page press Connect new service, pick Vercel under Data sources (inbound), fill Project ID and the Repo field, paste the token under Vercel access token, leave Scope at Team, press Connect. Press Test. Then rotate the old token at Vercel.

What you see. The card reads Connected with the identity, token followed by the first six and last four characters, and after the test, Last test: the provider answered. Within the hour each machine bound by an owner or admin shows synced from cloud on its Settings Integrations card, with the Replace and Remove buttons gone.

What it establishes. The token has one home. Each machine that holds it pulled it under an owner or admin binding and left a connector.credential_pull audit row naming the device and the mask. The machine bound by the engineer who left never held a binding that could pull.

Security events into Jira

Situation. The team wants each new security event as a Jira issue. Jira is at a public https site and an API token exists for a service account.

What you do. Press Connect new service, pick Jira under Integration bus (outbound), fill Jira site URL, Account email and Project key, paste the token under Jira API token, press Connect. Press Test and accept the confirmation, which says a synthetic RepoOps connector test event will create a visible item.

What you see. A private, loopback or http site URL is refused before the row is written, with blocked_egress_target. The test creates one issue in the project and the note reads Live test passed. From then on the hourly dispatch sweep at fifteen past the hour posts each new event for your team, at most fifty per fire.

What it establishes. The target is stored with its credential encrypted and its URL checked twice, at store time and again at each send. Nothing is sent until an event lands for your team, and the sweep is the only sender.

Before you start

Supported versions
RepoOps desktop v0.3.1 for the synced from cloud badge and the scheduled pull; the hosted page carries no version and reads the current deployment.
Where it runs
Hosted: repoops.ai, then Connections in the sidebar, under Workspace tools, which opens /team/connectors. Desktop: no page of its own; the desktop shows the synced from cloud badge on the Settings Integrations card for a credential the vault delivered.
Permissions
Every member of the team sees the safe view and the line read-only, managed by an org-admin. Connect, Test, Edit identity, Re-auth and Disconnect take the owner or admin role. Company scope takes the owner role, and so does editing or removing a Company row. A bound machine pulls a credential only when the user who bound it holds owner or admin on the team; a member's device gets 403 and the tick treats it as nothing to sync. The Tech-stack access panel for a Cloudflare connector renders for an admin.
Connections
REPOOPS_CONNECTOR_VAULT_KEY set on the hosted deployment by the operator, 32 bytes as base64. For an OAuth connect, the provider's own client id and secret pair on the deployment (GitHub, Xero, Ramp, Stripe). For a machine to pull, a device binding from /team/connect. For an outbound target, a public https URL.
Plan
The hosted tier or above (TEAM_SURFACE_MIN_TIER is hosted). Every route checks it before the role, so a lapsed team is refused even as owner, and a bound machine with a live device token is refused at the pull.

Configure it

  1. Have the operator set the vault key.

    Until REPOOPS_CONNECTOR_VAULT_KEY is set the page reads The connector vault is dormant and every write answers 503; no credential is ever written. A key that is set but not clean base64 gets its own sentence, and the page tells the operator to remove the stray character, not to rotate, because rotating makes every stored credential unreadable.

  2. Open Connectors and press Connect new service.

    The form is Connect a new service: a Provider select with two groups, Data sources (inbound) and Integration bus (outbound), then the provider's identity fields, Label (optional) and Feeds (optional), the token field named for the provider, and Scope. Team is the default; Company appears only for an owner.

  3. Give an inbound source its repo.

    Repo (optional, links this source to a repo) is what the hourly pull keys on. A source with no repo is stored and never read on the sweep (no_repo_key), which is a legitimate team-wide configuration and also the usual reason the Open-risks column stays empty. PostHog also needs Event allowlist (comma-separated; required for a pull).

  4. Or connect by OAuth where the deployment allows it.

    For GitHub, Xero, Ramp and Stripe a Connect with OAuth link appears when the operator has registered that vendor app. The link goes to the vendor and back within ten minutes; the callback re-checks your session, your role and the tier before it writes. Xero, Ramp and Stripe have no paste field at all: a pasted key would go around the reviewed scope. Stripe stores your account id and no secret.

  5. Press Test, and read which check ran.

    Live test passed means the provider answered one read with the stored credential. An outbound target is tested by sending one synthetic event, after a confirmation, because for Jira or Teams that creates a visible item. A provider with no live probe reads Credential decrypts, and the card then says the provider was not contacted; the Connected badge comes from create time and does not mean anyone answered. A failed test sets Needs re-auth.

  6. Bind the machines that should read it.

    A machine bound by an owner or admin pulls the Vercel, Sentry, PostHog and Anthropic credentials ten seconds after boot and then once an hour on the hour, writing VERCEL_TOKEN, SENTRY_AUTH_TOKEN, POSTHOG_PERSONAL_API_KEY or ANTHROPIC_API_KEY plus a REPOOPS_CONNECTOR_<id>_SOURCE=cloud marker into the data-dir .env. GitHub keeps the token from its own desktop flow; Postgres has nothing to write.

SettingWhereA sensible choiceWhy it matters
REPOOPS_CONNECTOR_VAULT_KEYthe hosted deployment's environment, set by the operator32 random bytes as base64, from the documented generatorUnset or under 16 characters and the vault is dormant in every environment. Not clean base64 and it is refused with its own sentence. Rotating it makes every stored credential unreadable.
REPOOPS_CONNECTOR_<PROVIDER>_CLIENT_ID and _CLIENT_SECRETthe hosted deployment's environment, one pair each for GITHUB, XERO, RAMP, STRIPEset both halves, or neitherEither half alone reads as unset, and an unset provider is absent from the OAuth list rather than a button that fails at the vendor. The scopes are fixed in the registry and read-only.
ProviderConnect a new service, the Provider selectan inbound source for something RepoOps should read, an outbound target for somewhere it should postThe family decides the gates: an outbound target's URL passes the egress guard at store time and at each send; an inbound identity may name an internal host.
ScopeConnect a new service, the Scope selectTeam (the default)Company is offered to an owner only and, for a row connected before the reach column, reaches the same machines Team does until it is re-connected. It records that the credential is meant org-wide.
Repo (optional, links this source to a repo)the identity fields of an inbound sourcethe repository slug the source belongs to, for example acme/webThe hourly pull maps a source to a repo by this field and skips a source without one. Required for a Vercel log drain.
Event allowlist (comma-separated; required for a pull)the PostHog identity fields$exception and the events you treat as failuresThe PostHog read returns nothing without a non-empty list.
Label (optional) and Feeds (optional)Connect a new service, and Edit identitywhich account this is, and what it feedsNotes shown on the card, guarded like the identity. They change nothing about what is collected.
REPOOPS_CONNECTOR_SYNC_ENABLEDa bound machine's data-dir .envleave unset (on)0, false, off or no stops that machine pulling. Any other value, including unset, leaves the hourly pull on; an unbound machine makes no request at all.
ⓘ
To stop or undo
Disconnect deletes the row after a confirmation that says bound machines stop syncing it; the audit row connector.disconnect records who. To stop one machine, set REPOOPS_CONNECTOR_SYNC_ENABLED=0 in its data-dir .env. To stop an OAuth connection at the source, revoke the grant at the vendor as well; Disconnect does not call the vendor. Nothing removes a value already written to a machine's data-dir .env; see Delete below.

What you should see

The normal case

Configuration. Vault key set. A Vercel source with Project ID and Repo, Scope Team, tested once. Two machines bound, one by an admin and one by a member.

Expect. The card reads Connected, the identity, the mask and Last test: the provider answered. The admin's machine shows synced from cloud within the hour; the member's machine shows its own local token state and nothing from the vault. The pull at one minute past the hour reads Vercel with the stored credential and writes the repo's open-risk count.

Verify. The count line above the grid reads 1 connectors, 1 connected. The audit log carries connector.upsert, connector.test and one connector.credential_pull per pull, each with the provider and the mask and never the secret. The hosted Repos page lights the Open-risks column for that repo after the first sweep.

Dormant, or no vendor app

Configuration. REPOOPS_CONNECTOR_VAULT_KEY unset, or set with a stray quote; or Xero picked on a deployment with no Xero client pair.

Expect. The page reads The connector vault is dormant and names which fault it is. With the vault live but no Xero app, the form says Xero is connected by OAuth only and this deployment has no Xero app registered yet, and offers no Connect button. With no OAuth provider at all it says every connector here is a pasted token.

Verify. POST /api/team/connectors answers 503 vault_not_configured with the same sentence the page shows. The OAuth start route answers 503 for an unregistered provider with one message, on purpose, so a prober cannot tell which vendor apps exist.

A failed test, or a check that was not live

Configuration. Any stored source whose token was revoked at the provider, or a provider with no live probe.

Expect. The revoked token fails its one read with the status in the reason, the row flips to Needs re-auth, and a Re-auth button appears with the field Re-authenticate. The provider with no probe passes as Credential decrypts and the card reads Last test: credential only, the provider was not contacted.

Verify. The audit row connector.test carries passed, check and the reason. Re-authenticate re-encrypts the new token and returns the row to Connected; an identity edit never touches the credential. An OAuth token near expiry is refreshed on read, 120 seconds before it expires, and a failed refresh sets Needs re-auth without deleting the envelope.

Data and cost

What is captured
One row per connector in team_connectors: provider, scope, reach, the identity JSON in plaintext, the credential as a vault.v1 envelope (AES-256-GCM under the vault key, 64 KiB cap), a mask (the first six and last four characters, or four dots for a value of ten characters or fewer; oauth and the scope for an OAuth grant), label, feeds, status, the last test time and verdict, and who connected it. Each write, test, pull and OAuth outcome leaves an audit row with the provider and the mask.
Who can see it
Every member of the team sees the safe view. A sibling team in the same company sees a Company row connected after the reach column as a read-only borrowed card, managed by the team that owns it. A machine bound by an owner or admin receives the plaintext of a Vercel, Sentry, PostHog or Anthropic credential once per pull over TLS, at most twelve pulls a minute per binding; an OAuth envelope is refused with oauth_not_exportable. The hosted side decrypts in memory for a test, the hourly pull, the hourly dispatch, the Cloudflare access read and the Financial Model syncs, and never returns it.
How long it is kept
No retention window and no purge job. A row lives until Disconnect deletes it or the team is deleted, which cascades. Audit rows follow the audit log's own rules.
What leaves the machine
To the provider, with the decrypted credential: one read on Test, the pull at one minute past each hour for Vercel, Sentry, PostHog and GitHub sources that name a repo, the Cloudflare membership read at three minutes past each hour, and the Financial Model's own syncs for Xero, Ramp and Stripe. To an outbound target: one synthetic event on Test, and each new security event for your team on the sweep at two minutes past the hour, labels and counts only. To a bound machine: the four syncable credentials. No model call.
What it costs
No model call and no metered cost. The pulls and sweeps run on the hosted deployment's crons. The tab itself needs the hosted tier; there is no connector-specific price.

When the result differs

SymptomLikely causeNext action
The connector vault is dormant.REPOOPS_CONNECTOR_VAULT_KEY is unset, under 16 characters, or set but not clean base64; the page says which.The operator sets the documented 32-byte base64 form, or removes the stray quote, space or line break. Never rotate to clear it.
No Connect with OAuth link, or Xero, Ramp or Stripe offers nothing to connect.That provider's client id and secret pair is not set on the deployment, or only one half is.The operator registers the vendor app with the callback the env file names and sets both halves. Until then the provider is absent rather than broken.
Test failed and the row reads Needs re-auth.The provider answered 401, 403, another non-2xx, or timed out on the one read.Read the reason on the note, then press Re-auth and paste the new token. For an outbound target, check the URL is public https and the destination accepted the synthetic event.
Last test: credential only, the provider was not contacted.This provider has no live probe, so the test proved the envelope decrypts and nothing more.Nothing to fix. Treat Connected as a statement about the row, not about the provider.
A machine never shows synced from cloud.It is unbound, bound by a member, has REPOOPS_CONNECTOR_SYNC_ENABLED=0, or the connector is not Vercel, Sentry, PostHog or Anthropic.Bind it under an owner or admin, or accept that the provider does not sync: GitHub keeps its own desktop token and Postgres stores only a path.
The Open-risks column stays empty for a source that tests fine.The source has no Repo field, or a PostHog source has no event allowlist, so the hourly pull skips it.Press Edit identity, fill Repo (optional, links this source to a repo), and for PostHog the Event allowlist, then Save identity.
Only a team owner can put a connector at Company scope.An admin asked for Company; the server resolves scope from the membership row, not the form.Choose Team, or ask an owner to connect it.
A Company card says it currently reaches only this team.The row was connected before the reach column, and nothing widens a stored credential on deploy.Re-connect it as an owner to give it company reach; leaving it alone keeps it team-only.
Disable
Disconnect on the card, after the confirmation. Per machine, REPOOPS_CONNECTOR_SYNC_ENABLED=0 in the data-dir .env. For the whole deployment, unsetting the vault key makes every row unreadable and every write refused, and nothing is deleted.
Roll back
Not provided. Re-authenticate overwrites the envelope in place and key_version stays at 1, so the previous credential is gone; paste the old token again if it is still valid at the provider. The data is the row in team_connectors.
Revoke access
Disconnect removes the row and calls no vendor, so revoke the token or the OAuth grant at the vendor as well. A bound machine's reach ends when its device is disconnected in the desktop app or its user loses the owner or admin role; the next pull answers 403. A synced value already on a machine stays in its data-dir .env.
Delete
Disconnect is a hard delete of the row. It does not remove the audit rows, or the open-risk counts, receipts and cases the source produced. Not provided: removing the value already written to a bound machine's data-dir .env. Nothing in lib/connectors/sync.mjs removes a key it wrote; the machine's operator edits the file, and the Settings card hides Replace and Remove while the cloud marker is present.

Maintenance evidence

Feature id
connectors (spine leaf connectors)
Owner
GitHub connect + company rollup + connector sync program (Phase 3, PRs 3.1 to 3.4); the customer financial connectors (G030 and G031); the outbound integration bus (WS6-D); company reach (G080). Guide: LDG-0717.
Supported product version
RepoOps v0.3.1
Last verified
2026-09-15, read against origin/main at 52366bb6d; labels read from the served tab source (website/app/team/(home)/connectors/page.tsx, connectors-manager.tsx, provider-catalog.ts) and the desktop badge from public/settings.html; the routes, the vault and crypto modules, the OAuth registry, the desktop sync and tick modules, the cron schedule in website/vercel.json and website/.env.example read in full. No live instance was run.
Example fixtures
No fixture file; the row shapes are inline in website/lib/connectors/vault.integration.test.ts, connectors.isolation.integration.test.ts, reach.integration.test.ts, crypto.test.ts, scope.test.ts, test-result.test.ts, egress.test.ts, identity-guard.test.ts, oauth-spine.test.ts and oauth-access.integration.test.ts; the route tests under website/app/api/team/connectors/ (the device-token, egress-guard, oauth-only, credential, update and callback tests); website/app/team/(home)/connectors/connectors-manager.test.tsx; and lib/connectors/sync.test.mjs and tick.test.mjs for the desktop pull.
Source references
website/lib/connectors/crypto.ts, website/lib/connectors/vault.ts, website/lib/connectors/scope.ts, website/lib/connectors/oauth-registry.ts, website/lib/connectors/oauth-access.ts, website/lib/connectors/identity-guard.ts, website/lib/connectors/egress.ts, website/lib/connectors/pull.ts, website/lib/connectors/dispatch.ts, website/app/api/team/connectors/route.ts, website/app/api/team/connectors/[id]/credential/route.ts, website/app/team/(home)/connectors/page.tsx, website/app/team/(home)/connectors/connectors-manager.tsx, website/app/team/(home)/connectors/provider-catalog.ts, lib/connectors/sync.mjs, lib/connectors/tick.mjs, public/settings.html
Documentation review
Independent review requested on the slice pull request; not yet recorded.
Video review
Story script written 2026-09-15; render and review pending in the same slice.

Last updated