Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt

Use this file to discover all available pages before exploring further.

The GitOps endpoints turn a tenant’s decisioning configuration (offers, creatives, channels, qualification rules, contact policies, decision flows, arbitration profiles) into a reviewable YAML/JSON bundle and reconcile a posted bundle back into the database. Reconciliation matches resources by metadata.name rather than by id, so the same bundle promotes across dev / stage / prod where ids differ (src/lib/gitops/types.ts:22-23).

What it does

Three routes share the same ResourceBundle shape declared at src/lib/gitops/types.ts:57-68:
  • GET /api/v1/gitops/export — dump the tenant’s current state as YAML or JSON, optionally split into per-resource files.
  • POST /api/v1/gitops/diff — compute the diff between a posted bundle and the live DB without mutating.
  • POST /api/v1/gitops/apply — reconcile a posted bundle into the DB, dry-run by default.
A separate scheduled cron at /api/v1/cron/gitops-drift-check runs the diff against a stored snapshot and surfaces drift through cron.schedules.gitopsDriftCheck in helm/values.yaml. See Cron tier. V1 reconciler scope (per src/lib/gitops/apply.ts:15-19): Category, Channel, ArbitrationProfile, Offer, DecisionFlow. Other resource kinds (SubCategory, Creative, QualificationRule, ContactPolicy) are recognized and validated but report as unchanged — the apply hooks for those kinds are pending.

Quick start

Round-trip a tenant through git:
# 1. Export current state to YAML
curl https://playground.kaireonai.com/api/v1/gitops/export \
  -H "X-API-Key: krn_your_api_key" \
  -H "X-Tenant-Id: 5a9904b9-..." \
  -o tenant-bundle.yaml

# 2. Edit a file, commit to git, then preview the diff
curl -X POST https://playground.kaireonai.com/api/v1/gitops/diff \
  -H "Content-Type: application/yaml" \
  -H "X-API-Key: krn_your_api_key" \
  --data-binary @tenant-bundle.yaml

# 3. Apply for real (dryRun=false)
curl -X POST 'https://playground.kaireonai.com/api/v1/gitops/apply?dryRun=false' \
  -H "Content-Type: application/yaml" \
  -H "X-API-Key: krn_your_api_key" \
  --data-binary @tenant-bundle.yaml

How it works

Bundle format

A ResourceBundle is a manifest plus a flat resources[] array. Each resource follows a Kubernetes-like shape (src/lib/gitops/types.ts:9-25):
apiVersion: kaireonai.com/v1
kind: Offer
metadata:
  name: gold-card-application       # natural key for reconciliation
  annotations:
    kaireonai.com/version: "3"
    kaireonai.com/id: "..."         # optional DB id hint
spec:
  # entity-specific fields
The apiVersion constant lives at src/lib/gitops/types.ts:27. Reconciliation matches by metadata.name, never by id, so the same bundle is promotable across environments.

Authentication

Every route calls requireTenant(request) (apply/route.ts:54, diff/route.ts:39, export/route.ts:21). The tenant binds via X-API-Key (preferred — also used as the rate-limit identifier) or X-Tenant-Id. Missing tenant returns 401; invalid tenant returns 403. The current implementation does not enforce a role gate beyond tenant binding — a viewer-tier API key can apply changes. Production deployments wire role enforcement via an upstream proxy or by extending the route to call requireRole.

Audit trail

Real applies (dryRun=false) write a single gitops_apply row to audit_logs summarizing { created, updated, unchanged, kinds } (apply/route.ts:76-95). The audit write is best-effort — if it fails the apply still returns success.

Diff-then-apply pattern

POST /api/v1/gitops/diff is the same code path as POST /api/v1/gitops/apply?dryRun=true (diff/route.ts:55-56 calls diffBundle which delegates to applyBundle({ apply: false }) at apply.ts:489-495). The dedicated diff endpoint is convenience — no body parsing differences, identical validation.

Reference

POST /api/v1/gitops/apply

Reconciles a posted bundle into the tenant’s DB. Dry-run by default.

Body formats accepted

The route reads the request body with await request.text() and dispatches based on Content-Type (apply/route.ts:26-48):
  • Content-Type: application/yaml (or any value containing yaml) — body is parsed as YAML by parseBundleYaml from src/lib/gitops/serialize.ts.
  • Content-Type: application/json with a top-level yamlText field — the field is unwrapped and YAML-parsed.
  • Content-Type: application/json with a bare ResourceBundle JSON object — parsed by parseBundleJson.
  • Any other content type — parsed first as JSON, then as YAML on JSON failure.
An empty body returns 400 Invalid bundle: Empty request body.

Query parameters

dryRun
string
default:"true"
When "false", mutations are committed and an audit row is written. Any other value (including absent) is treated as a dry run (apply/route.ts:58-59).
prune
string
default:"false"
Reserved — when true, the reconciler deletes resources that exist in DB but not in the bundle. Currently surfaced via ApplyOptions.prune in lib/gitops/apply.ts:38 but not enforced for every kind in V1.
kinds
string
Comma-separated ResourceKind filter — only the listed kinds are reconciled. Valid values per src/lib/gitops/types.ts:29-38: Category, SubCategory, Offer, Creative, Channel, QualificationRule, ContactPolicy, DecisionFlow, ArbitrationProfile.

Response

Returned at apply/route.ts:97. Shape declared at src/lib/gitops/types.ts:83-95.
{
  "dryRun": false,
  "diffs": [
    {
      "kind": "Offer",
      "name": "gold-card-application",
      "op": "update",
      "changedFields": ["priority", "businessValue"]
    },
    {
      "kind": "Channel",
      "name": "email",
      "op": "unchanged"
    }
  ],
  "summary": {
    "created": 1,
    "updated": 1,
    "deleted": 0,
    "unchanged": 14
  }
}
dryRun
boolean
Mirrors the dryRun query parameter — true for a preview, false when changes were committed.
diffs
array
Per-resource diff. Each entry is { kind, name, op, changedFields?, before?, after? } per src/lib/gitops/types.ts:72-81. op is one of "create" | "update" | "delete" | "unchanged".
summary
object
Counts across all diffs: { created, updated, deleted, unchanged }.

Status codes

CodeWhen
200Apply or dry-run succeeded
400Invalid bundle (parse error, empty body, schema violation)
401Missing tenant context
403Invalid tenant identifier
500Reconciler raised — full message in error.message

POST /api/v1/gitops/diff

Computes the diff against the live DB without mutating. Same body formats as apply.

Query parameters

kinds
string
Same comma-separated filter as apply. Other apply-only flags (dryRun, prune) are not read by this route.

Response

Same ApplyResult shape as apply with dryRun: true always (diff/route.ts:55-56).

Status codes

CodeWhen
200Diff computed
400Invalid bundle
401 / 403Missing or invalid tenant context
500Reconciler raised

GET /api/v1/gitops/export

Dumps the tenant’s current decisioning state as a ResourceBundle.

Query parameters

format
string
default:"yaml"
yaml (default) or json. Read at export/route.ts:25.
layout
string
default:"bundle"
bundle (default) returns one document. files returns a map of relative path → file content via bundleToFiles from lib/gitops/serialize.ts, suitable for writing to disk for git review (export/route.ts:31-46).

Response headers

bundle layout sets Content-Disposition: attachment; filename="kaireon-export-<tenantId>.<ext>" (export/route.ts:42, 53, 60). YAML responses use Content-Type: application/yaml.

Response body

Bundle layout (default) — a single YAML or JSON document per ResourceBundle shape:
manifest:
  apiVersion: kaireonai.com/v1
  kind: Bundle
  tenant: 5a9904b9-...
  exportedAt: "2026-04-30T14:00:00.000Z"
  resourceCounts:
    Offer: 12
    Channel: 4
    DecisionFlow: 1
resources:
  - apiVersion: kaireonai.com/v1
    kind: Offer
    metadata:
      name: gold-card-application
    spec:
      priority: 80
      businessValue: 100
      ...
Files layout — JSON response with a files map plus the original manifest:
{
  "files": {
    "bundle.yaml": "...",
    "offers/gold-card-application.yaml": "...",
    "channels/email.yaml": "..."
  },
  "manifest": { "apiVersion": "kaireonai.com/v1", "kind": "Bundle", ... }
}

Status codes

CodeWhen
200Export succeeded
401 / 403Missing or invalid tenant context
500Export raised — full message in error.message

Required headers

HeaderRequiredRead atPurpose
Content-TypeYes (apply / diff)Next.js frameworkapplication/yaml, application/json, or any value containing yaml
X-API-KeyYes (one of the two)tenant.ts:97API key (krn_…)
X-Tenant-IdYes (one of the two)tenant.ts:113Direct tenant id

Configuration

Drift detection

/api/v1/cron/gitops-drift-check runs the diff against a stored bundle snapshot on a schedule defined at helm/values.yaml:
cron:
  schedules:
    gitopsDriftCheck:
      enabled: true
      schedule: "0 5 * * *"
      path: "/api/v1/cron/gitops-drift-check"
The cron does not run apply — it only surfaces drift through metrics and audit. See Cron tier.

File layout for git

Recommended directory structure when using layout=files:
tenant/
├── bundle.yaml
├── categories/
├── channels/
├── decision-flows/
├── offers/
└── arbitration-profiles/
The exact filenames are returned by bundleToFiles — operators do not need to invent the layout.

Honest limits

  • Reconciler scope in V1 covers Category, Channel, ArbitrationProfile, Offer, DecisionFlow (apply.ts:15-17). The other recognized kinds (SubCategory, Creative, QualificationRule, ContactPolicy) parse and validate but always report as unchanged — adding them is a per-kind case in applyResource.
  • The current routes enforce tenant binding but not a role gate. A viewer-tier API key can call apply with dryRun=false. Production deployments either wire role enforcement upstream or extend the route to call requireRole(request, "admin", "editor").
  • prune=true is accepted as a query parameter and threaded into ApplyOptions.prune, but full prune-by-kind enforcement is not yet implemented for every kind. Treat prune as opt-in and verify behavior per kind before enabling.
  • The audit-log write inside the apply path is best-effort and silenced on failure (apply/route.ts:91-95). The reconciliation itself still commits — if the audit row matters for compliance, monitor audit_logs ingestion separately.
  • An empty request body is rejected with 400 Invalid bundle: Empty request body, but a body containing only {} will pass through to the YAML parser fallback and surface a less specific error. Senders should always include the full manifest + resources shape.
  • Cron tier — runs the daily /api/v1/cron/gitops-drift-check.
  • Audit Logsgitops_apply action rows record every real apply.
  • Decision Flows — one of the reconciler-supported resource kinds.
  • Offers — one of the reconciler-supported resource kinds.