Key points
- Two scopes, one shape: the global repository holds system settings, a team repository holds that team’s pipelines, steps, schedules, scopes, and triggers. Both expose read, sync, drift, write, and validate.
- A sync is asynchronous and singular. Starting one while another runs answers 409 — that is the platform refusing to run two, not a bad request.
- Cancelling mid-sync leaves a partial import rather than rolling back.
- Disconnecting a repository does not remove configuration; it removes the source of truth, and the documents become database-owned.
- Drift carries both
git_contentanddesired_contentper file, which is what the side-by-side review renders. - The write routes are the only ones here that change something outside the platform.
Operations
GET/v1/system/config-repoAuthorized
Reads the global configuration repository connection.
Notes
A disconnected install answers 200 with an empty document rather than 404, so a client can read unconditionally.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config-repo" | jqResponses
The configured global repository, or an empty document when none is connected.
{
"repository": "https://git.example.com/platform/nopsai-config",
"branch": "main",
"base_path": "",
"credential_ref": "platform/git-config"
}Side effects
- None.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_repositories_schema.go
PUT/v1/system/config-repoAuthorized
Connects or replaces the global configuration repository.
Call it
curl -sX PUT "$NOPSAI_URL/v1/system/config-repo" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repository":"https://git.example.com/platform/nopsai-config","branch":"main","credential_ref":"platform/git-config"}' | jqResponses
Connection stored.
{
"repository": "https://git.example.com/platform/nopsai-config",
"branch": "main",
"credential_ref": "platform/git-config"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | An unreachable repository, an unknown branch, or a credential that cannot read it. | The connection is checked before it is stored. |
| 500 | The connection could not be saved. | Retry. |
Side effects
- Stores the connection. Configuration is imported only when a sync runs.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_repositories_schema.go
DELETE/v1/system/config-repoAuthorized
Disconnects the global configuration repository.
Notes
Disconnecting does not remove configuration. It removes the source of truth, which is a different and easily surprising thing.
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config-repo" -w "%{http_code}\n"Responses
Disconnected.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 500 | The disconnect could not be persisted. | Retry. |
Side effects
- Imported documents remain in the database and become database-owned.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_repositories_schema.go
POST/v1/system/config/syncAuthorized
Starts a configuration sync across every connected repository.
Notes
A sync is asynchronous and singular: the 409 is the platform refusing to run two at once, not an error in your request.
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config/sync" | jqResponses
Sync started.
{"state":"running"}When it fails
| Status | Cause | What to do |
|---|---|---|
| 409 | A sync is already running. | Poll the status route rather than starting a second one. |
Side effects
- Imports pipelines, steps, schedules, scopes, triggers, knowledge, dashboards, and system settings from Git.
- Documents removed from Git are removed from the platform.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_sync.go
GET/v1/system/config/syncAuthorized
Reads the current sync status.
Notes
Safe to poll: it reads recorded state and does not touch Git.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config/sync" | jqResponses
Sync status.
{
"state": "idle",
"last_sync_at": "2026-08-19T09:58:12Z",
"last_commit_sha": "4f2a91c",
"message": "synced 42 documents"
}Side effects
- None.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_sync.go
POST/v1/system/config/sync/cancelAuthorized
Cancels a running sync.
Notes
Cancelling mid-sync leaves a partial import rather than rolling back. Run another sync to reach a consistent state.
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config/sync/cancel" | jqResponses
Cancellation accepted.
{"state":"cancelled"}Side effects
- Stops the run; already-imported documents remain.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_sync.go
GET/v1/system/config-repo/syncAuthorized
Reads sync status for the global repository specifically.
Notes
Use this when several repositories are connected and you need to know which one failed.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config-repo/sync" | jqResponses
Global repository sync status.
{
"state": "idle",
"last_sync_at": "2026-08-19T09:58:12Z",
"last_commit_sha": "4f2a91c",
"message": "synced 42 documents"
}Side effects
- None.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_sync.go
POST/v1/system/config-repo/syncAuthorized
Syncs the global repository only.
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config-repo/sync" | jqResponses
Sync started for the global repository.
{"state":"running"}When it fails
| Status | Cause | What to do |
|---|---|---|
| 409 | A sync is already running. | Poll the status route. |
Side effects
- Imports system settings and global documents.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_sync.go
POST/v1/system/config-repo/sync/cancelAuthorized
Cancels the global repository sync.
Notes
As with the platform-wide cancel, a partial import is left in place.
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config-repo/sync/cancel" | jqResponses
Cancellation accepted.
{"state":"cancelled"}Side effects
- Stops the run; already-imported documents remain.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_sync.go
GET/v1/system/config-repo/driftAuthorized
Compares Git against desired state for the global repository.
Notes
Each item carries git_content and desired_content, which is what the UI renders as a side-by-side diff before a push.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config-repo/drift" | jq '{summary, can_push}'Responses
Drift between the repository and the platform.
{
"base_branch": "main",
"push_branch": "nopsai/config-sync",
"items": [
{
"path": "pipelines/platform/release-service.yaml",
"status": "modified",
"git_content": "name: release-service\n...",
"desired_content": "name: release-service\n..."
}
],
"summary": { "modified": 1 },
"can_push": true,
"push_message": "Sync 1 change from NopsAI"
}Side effects
- None. Reading drift never writes to Git.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_repository_drift.goservices/nopsai/internal/configsync/diff.go
POST/v1/system/config-repo/writeAuthorized
Pushes desired state back to the global repository.
Notes
This is the one route in the area that changes something outside the platform. Read drift first.
Call it
curl -sX POST "$NOPSAI_URL/v1/system/config-repo/write" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"push_message":"Sync settings from NopsAI"}' | jqResponses
Changes written to the push branch.
{
"branch": "nopsai/config-sync",
"commit_sha": "9b31f0c",
"written": 1
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | Nothing to write, or the drift cannot be pushed. | Check can_push and push_message on the drift response. |
| 500 | The push failed at the provider. | Check the credential’s write access to the push branch. |
Side effects
- Commits to the configured push branch of a real repository.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_repository_drift.go
POST/v1/system/config-repo/validateAuthorized
Validates configuration documents without storing or pushing them.
Notes
Validating a whole file set at once is what a CI job should call on a configuration repository pull request.
Call it
curl -sX POST "$NOPSAI_URL/v1/system/config-repo/validate" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"base_path":"","files":[{"path":"setting/system/auth.yaml","content":"local_enabled: true\n"}]}' | jqResponses
Validation ran. Read valid rather than the status code.
{
"valid": true,
"errors": [],
"warnings": []
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The payload is not valid JSON or names an unusable base path. | Send base_path and a files list. |
Side effects
- None.
Proven by
services/nopsai/config_repositories_schema_test.goservices/nopsai/validation_handlers.go
GET/v1/system/config-reposAuthorized
Lists every connected configuration repository.
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config-repos" | jqResponses
All connected repositories.
[
{
"id": 1,
"scope": "global",
"repository": "https://git.example.com/platform/nopsai-config",
"branch": "main"
},
{
"id": 2,
"scope": "team",
"team_path": "platform",
"repository": "https://git.example.com/platform/team-config",
"branch": "main"
}
]When it fails
| Status | Cause | What to do |
|---|---|---|
| 500 | The query failed. | Platform fault. |
Side effects
- None.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_repositories_schema.go
POST/v1/system/config-repos/syncAuthorized
Syncs every connected repository.
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config-repos/sync" | jqResponses
Sync started.
{"state":"running"}When it fails
| Status | Cause | What to do |
|---|---|---|
| 409 | A sync is already running. | Poll the status route. |
Side effects
- Imports from every connected repository.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_sync.go
POST/v1/system/config-repos/sync/cancelAuthorized
Cancels the all-repository sync.
Notes
Partial imports are left in place; run another sync to converge.
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/system/config-repos/sync/cancel" | jqResponses
Cancellation accepted.
{"state":"cancelled"}Side effects
- Stops the run.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_sync.go
GET/v1/teams/{teamID}/config-repositoryAuthorized
Reads a team’s configuration repository connection.
Notes
A team repository owns that team’s pipelines, steps, schedules, scopes, and triggers. The global repository owns system settings.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
teamID | path | integer | Required | Team identifier. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/teams/$TEAM_ID/config-repository" | jqResponses
The team connection.
{
"repository": "https://git.example.com/platform/team-config",
"branch": "main",
"base_path": "",
"credential_ref": "platform/git-config"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 405 | A method the surface does not support. | GET, PUT, and DELETE are supported here. |
Side effects
- None.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/team_handlers.go
PUT/v1/teams/{teamID}/config-repositoryAuthorized
Connects or replaces a team’s configuration repository.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
teamID | path | integer | Required | Team identifier. |
Call it
curl -sX PUT "$NOPSAI_URL/v1/teams/$TEAM_ID/config-repository" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repository":"https://git.example.com/platform/team-config","branch":"main","credential_ref":"platform/git-config"}' | jqResponses
Connection stored.
{
"repository": "https://git.example.com/platform/team-config",
"branch": "main"
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The repository or credential cannot be used. | The connection is checked before it is stored. |
| 405 | An unsupported method. | Use GET, PUT, or DELETE. |
Side effects
- Stores the connection.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/team_handlers.go
DELETE/v1/teams/{teamID}/config-repositoryAuthorized
Disconnects a team’s configuration repository.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
teamID | path | integer | Required | Team identifier. |
Call it
curl -sX DELETE -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/teams/$TEAM_ID/config-repository" -w "%{http_code}\n"Responses
Disconnected.
When it fails
| Status | Cause | What to do |
|---|---|---|
| 405 | An unsupported method. | Use GET, PUT, or DELETE. |
Side effects
- Imported documents remain and stop being refreshed from Git.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/team_handlers.go
POST/v1/teams/{teamID}/config-repository/syncAuthorized
Syncs one team’s repository.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
teamID | path | integer | Required | Team identifier. |
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/teams/$TEAM_ID/config-repository/sync" | jqResponses
Sync started for the team.
{"state":"running"}When it fails
| Status | Cause | What to do |
|---|---|---|
| 409 | A sync is already running for this team. | Poll the team sync status. |
Side effects
- Imports the team’s pipelines, steps, schedules, scopes, and triggers.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/team_handlers.go
GET/v1/teams/{teamID}/config-repository/syncAuthorized
Reads a team’s sync status.
Notes
Safe to poll.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
teamID | path | integer | Required | Team identifier. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/teams/$TEAM_ID/config-repository/sync" | jqResponses
Team sync status.
{
"state": "idle",
"last_sync_at": "2026-08-19T09:58:12Z",
"last_commit_sha": "4f2a91c",
"message": "synced 42 documents"
}Side effects
- None.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/team_handlers.go
POST/v1/teams/{teamID}/config-repository/sync/cancelAuthorized
Cancels a team’s running sync.
Notes
Partial imports are left in place.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
teamID | path | integer | Required | Team identifier. |
Call it
curl -sX POST -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/teams/$TEAM_ID/config-repository/sync/cancel" | jqResponses
Cancellation accepted.
{"state":"cancelled"}Side effects
- Stops the run.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/team_handlers.go
GET/v1/teams/{teamID}/config-repository/driftAuthorized
Compares a team’s repository against desired state.
Notes
Drift appears whenever someone edits a GitOps-managed document through the UI or API: the database becomes the owner until it is pushed or discarded.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
teamID | path | integer | Required | Team identifier. |
Call it
curl -s -H "Authorization: Bearer $NOPSAI_TOKEN" "$NOPSAI_URL/v1/teams/$TEAM_ID/config-repository/drift" | jq '{summary, can_push}'Responses
Drift for the team repository.
{
"base_branch": "main",
"push_branch": "nopsai/config-sync",
"items": [
{
"path": "pipelines/platform/release-service.yaml",
"status": "modified",
"git_content": "name: release-service\n...",
"desired_content": "name: release-service\n..."
}
],
"summary": { "modified": 1 },
"can_push": true,
"push_message": "Sync 1 change from NopsAI"
}Side effects
- None.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_repository_drift.go
POST/v1/teams/{teamID}/config-repository/writeAuthorized
Pushes a team’s desired state back to Git.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
teamID | path | integer | Required | Team identifier. |
Call it
curl -sX POST "$NOPSAI_URL/v1/teams/$TEAM_ID/config-repository/write" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"push_message":"Sync platform pipelines from NopsAI"}' | jqResponses
Changes written.
{
"branch": "nopsai/config-sync",
"commit_sha": "1a7de40",
"written": 3
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | Nothing to write, or drift that cannot be pushed. | Check can_push on the drift response. |
| 500 | The push failed at the provider. | Check write access on the push branch. |
Side effects
- Commits to a real repository.
- Writes an audit record.
Proven by
services/nopsai/config_sync_test.goservices/nopsai/config_repository_drift.go
POST/v1/teams/{teamID}/config-repository/validateAuthorized
Validates a team’s configuration documents without storing them.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
teamID | path | integer | Required | Team identifier. |
Call it
curl -sX POST "$NOPSAI_URL/v1/teams/$TEAM_ID/config-repository/validate" \
-H "Authorization: Bearer $NOPSAI_TOKEN" \
-H "Content-Type: application/json" \
--data @files.json | jqResponses
Validation ran.
{
"valid": true,
"errors": [],
"warnings": []
}When it fails
| Status | Cause | What to do |
|---|---|---|
| 400 | The payload could not be read. | Send base_path and files. |
Side effects
- None.
Proven by
services/nopsai/config_repositories_schema_test.goservices/nopsai/validation_handlers.go
How it works
Drift is not a fault. It is what happens when someone edits a GitOps-managed document through the UI or API: the database becomes the owner until the change is pushed back or discarded. The question a reviewer answers is which of the two contents should win.
Validate a whole file set rather than a document at a time. That is the call a CI job should make on a configuration repository pull request, and it reports every issue with the file it came from.
When several repositories are connected, the per-repository status routes are how you find which one failed. The platform-wide status tells you a sync failed, not where.
Implementation evidence
services/nopsai/config_sync.goSync orchestration and status.
services/nopsai/config_repository_drift.goDrift comparison and push behaviour.
services/nopsai/internal/configsync/diff.goPer-file drift item shape.

