Workspace governance
FetchCatch lets a tenant edit flows from two places: the web console and the fcc CLI. To
keep the question "who is the source of truth?" answered before it's ever asked, every workspace
declares a governance mode that gates the publish event.
Editing drafts is unrestricted in all modes. The governance mode only controls who is allowed to mint a new published version — i.e. who can change what
/v1/evaluate/{slug}returns.
The three modes
| Mode | Designer publish | CLI publish | Best for |
|---|---|---|---|
DesignerOnly |
✅ allowed | ❌ refused (HTTP 400) | Solo founders, no-code teams, demos |
CodeManaged |
❌ refused (HTTP 400) | ✅ allowed | Teams with PR review + CI/CD |
Hybrid |
✅ allowed | ✅ allowed | Default. Power-user opt-in; three-way merge + conflict detection apply |
The mode is stored as a column on Workspaces and enforced on every publish attempt by the
GovernanceEnforcer service. An Owner/Admin can change it at any time from
Settings → Workspaces → Settings without restarting anything.
What "publish" means
Publishing snapshots the current draft graph as an immutable, numbered version and sets
Flows.PublishedVersionId to point at it. From that moment on, every call to
POST /v1/evaluate/{slug} returns the new version's decisions.
The publish event has provenance: every FlowVersions row carries
Source— one ofdesigner-publish,cli-publish,cli-apply,designer-draft,rollbackCreatedByEmail— actor email at the time of the writeGitCommitSha— commit SHA the CLI advertised viaX-FCC-Git-Commit
This shows up in three places:
- Console flow history panel (sidebar opened from the designer) — two tabs,
Published (only events that changed
/v1/evaluateoutput) and All snapshots (every save/apply). Each row renders the actor as a colored initials chip, the source as a semantic badge, and the git commit short-SHA as a link to the workspace's configured repo URL (github.com,gitlab.com,bitbucket.orgsupported). - Console Runs dashboard — every row now shows the targeted version's number,
source badge (
cli/console/cli·draft/console·draft), and short commit SHA. A Source: cli / console / all filter chip sits next to the status chips. - CLI:
fcc history flows/<slug>.json— full save/apply log withcommitcolumn.fcc history flows/<slug>.json --publishes— only publishes; marks the live version with*.
Querying provenance from your tools
The publish timeline is exposed as a stable JSON endpoint so you can plug it into release notes, Slack notifications, or dashboards without screen-scraping:
curl -H "Authorization: Bearer $FETCHCATCH_TOKEN" \
https://api.fetchcatch.com/v1/sync/history/flow/<flowId>/publishes
Response (each item):
{
"version": 17,
"publishedAtUtc": "2026-05-26T14:31:02Z",
"source": "cli-publish",
"createdByEmail": "ci-bot@acme.com",
"gitCommitSha": "a1b2c3d4e5f6789",
"isCurrent": true
}
Choosing a mode
DesignerOnly
- Console publishes only.
fcc applyandfcc publishare refused with: "This workspace is set to DesignerOnly. Publishing from the CLI is disabled. …"- Use when you want a clean "edit in the console, no scripting required" experience.
- You can still run
fcc pullto back up the workspace into git as read-only.
CodeManaged
- CLI publishes only. The console shows the draft and lets you edit it (useful for inspection and last-mile tweaks before committing), but the Publish button is disabled with a banner pointing at your repo URL.
- Use when every change should go through a pull request — the same workflow you have for code.
- CI runs
fcc publishafter merge to ship the new version. The commit SHA is stamped onto the resultingFlowVersionsrow for audit.
Hybrid
- Both surfaces are allowed.
- Use when you want the speed of in-console fixes during incidents but also git history for the long-run audit trail.
- Risk: changes from one surface can shadow the other. Mitigate with:
- The recently-pulled banner in the designer (already wired in).
- The "diverged from main" badge +
fcc driftexit-code 2 (see below).
Drift detection
GET /v1/sync/drift (also fcc drift from CI) returns a small JSON payload:
{
"workspace": { "slug": "billing-prod", "governanceMode": "Hybrid", "environmentKind": "Production" },
"git": { "branch": "main", "lastCommitSha": "a1b2c3d4…", "lastCommitSyncedAtUtc": "…" },
"pending": { "push": 0, "pull": 0, "conflicts": 0 },
"designerPublishesSinceLastGitCommit": 2,
"inSync": false,
"checkedAtUtc": "…"
}
designerPublishesSinceLastGitCommit > 0 means the console minted a published version that has
no matching git commit. That's the canonical "someone hot-fixed in production" signal.
Wire fcc drift into a nightly job:
- name: Check for designer drift
run: fcc drift
env:
FETCHCATCH_TOKEN: ${{ secrets.FETCHCATCH_TOKEN }}
CI: "true"
Exit codes:
0— in sync, no drift2— drift detected or local/remote pending changes3— conflicts (local + remote both changed since last pull)1— error
API reference
| Verb | Route | Notes |
|---|---|---|
PATCH |
/v1/workspaces/{id} |
{ governanceMode?, environmentKind?, gitRepoUrl?, gitBranch?, name? }. Owner/Admin only. |
POST |
/v1/sync/publish |
Body { flowIds: [] } publishes every flow with a draft; non-empty list publishes only those. |
GET |
/v1/sync/drift |
CI-friendly drift summary. |
GET |
/v1/sync/history/{type}/{id} |
All save/apply snapshots for a resource. Returns source, createdBy, gitCommitSha. |
GET |
/v1/sync/history/flow/{flowId}/publishes |
New. Just the publish events — isCurrent=true marks the live version. |
GET |
/v1/runs?source=cli|designer |
Runs list filtered by the targeted version's provenance family. Each row carries flowVersion, versionSource, versionGitCommitSha. |
Migration safety
Two migrations ship together:
1748300000_GovernanceAndProvenance.sqladds the columns. Every existing workspace defaults toGovernanceMode = 'Hybrid'(the pre-feature behavior). ExistingFlowVersionsget the catch-allSource = 'designer'so the audit trail still starts now and not retroactively.1748400000_ProvenanceBackfillAndIndexes.sqlruns immediately after and:- Splits the catch-all
'designer'into the precise'designer-publish'/'designer-draft'labels based onPublishedAtUtcso old rows render sensibly in the new history panel. - Joins
FlowVersions.CreatedBy → Users.Emailto backfillCreatedByEmailwhere the new column is still null. - Adds three indexes that make the new query shapes O(log n) instead of
full scans:
IX_FlowVersions_Workspace_Source_Commit— powers/v1/sync/drift.IX_FlowVersions_Flow_PublishedAt— powers the new/v1/sync/history/flow/{id}/publishesendpoint.IX_ResourceVersions_Resource_Version— coversfcc historyand the designer snapshots tab.
- Splits the catch-all
Both migrations are idempotent. No behavior changes until an Owner explicitly
switches a workspace to DesignerOnly or CodeManaged.