Skip to content

feat(reconcile): add a MetaSchema kind for declarative metaschema management - #1882

Merged
rohilsurana merged 10 commits into
mainfrom
feat/metaschema-reconcile-kind
Aug 14, 2026
Merged

feat(reconcile): add a MetaSchema kind for declarative metaschema management#1882
rohilsurana merged 10 commits into
mainfrom
feat/metaschema-reconcile-kind

Conversation

@rohilsurana

Copy link
Copy Markdown
Member

Summary

This adds a MetaSchema kind to the frontier reconcile flow. It manages the built-in entity metaschemas, the JSON schemas that validate the metadata field on users, organizations, groups, roles, and prospects, as declarative files. Before this, the only way to change a metaschema was a hand-run API call, with no file, no plan, and no record of what should exist.

Metaschemas are managed as values, mirroring predefined roles: the file sets the JSON schema for a built-in, and a built-in left out of the file resets to its shipped default. There is no delete.

Changes

  • Lift the default schemas into a shared core/metaschema.Defaults, one source for both the server seeding and the reconcile kind. Adding a metaschema for a new resource later is then a single edit.
  • Add the pure diff, validation, and export helpers in internal/reconcile/metaschema.go.
  • Add the MetaSchemaReconciler.
  • Register the MetaSchema kind in cmd/reconcile.go, which enables both frontier reconcile and frontier export metaschema.
  • Document it in the RFC and the reconcile guide, and list it in the CLI help.

Technical Details

  • The managed set is the five built-ins: user, group, organization, role, prospect. A built-in the file lists is set to its schema, one left out resets to its shipped default, and a name outside the set is rejected at validation. A built-in missing on the server is created.
  • The reset target is compiled into the reconcile binary, like predefined roles, because the database stores only the current schema, not the shipped default. The shared core/metaschema.Defaults keeps it to one source.
  • Schema comparison uses normalized JSON, so formatting never makes a false diff, and an export round-trips to zero changes (RFC rule 5).
  • The MetaSchema RPCs live on FrontierService, and UpdateMetaSchema keys by UUID, so the reconciler resolves name to id from ListMetaSchemas first.
  • The postgres store change is limited to MigrateDefaults, which now ranges over the shared map. Seeding behavior is unchanged.

Merge ordering

This should merge after #1878 (the metaschema cache refresh). Without that refresh, a schema change made through the API reaches only the pod that handled it, so on a multi-pod server the reconcile plan and applies would not be consistent across pods. The RFC edit here removes the "blocked on per-pod cache" future-work bullet, and the guide notes that a change propagates within the cache refresh interval. Both assume #1878 is deployed.

Test Plan

  • Build and type checking passes (go build ./..., go vet)
  • New unit tests pass under go test -race (core/metaschema, internal/reconcile), covering the diff (set, reset, create), validation (unknown name, duplicate, invalid JSON), JSON normalization (whitespace and key order), and the export round-trip

SQL Safety (touches metaschema_repository.go)

  • No query is constructed or changed. The only edit to metaschema_repository.go is MigrateDefaults ranging over the shared metaschema.Defaults map instead of a local one. The placeholder, ToSQL(), and goqu.L items do not apply.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added support for managing MetaSchema resources through reconciliation.
    • Included built-in metadata schemas for users, groups, organizations, roles, and prospects.
    • Added validation, dry-run previews, updates, exports, and restoration of omitted built-in schemas.
  • Documentation

    • Documented MetaSchema configuration, validation, reset, export, and caching behavior.
    • Updated reconciliation guidance and supported resource listings.

Walkthrough

The PR adds MetaSchema reconciliation with five embedded defaults. It validates and canonicalizes JSON schemas, plans create, update, and reset operations, integrates Frontier API calls, exports overrides, updates migration, and documents the new kind.

Changes

MetaSchema support

Layer / File(s) Summary
Built-in schema registry and migration
core/metaschema/*, internal/store/postgres/metaschema_repository.go
Adds five embedded JSON schemas and exposes them through metaschema.Defaults. Default migration uses the shared registry.
Validation, comparison, and export planning
internal/reconcile/metaschema.go, internal/reconcile/metaschema_test.go
Adds strict schema validation, canonical JSON comparison, create/update/reset planning, and export of non-default schemas.
Reconciler integration and documented contract
internal/reconcile/metaschema_reconciler.go, internal/reconcile/metaschema_reconciler_test.go, cmd/reconcile.go, docs/content/docs/reconcile.mdx, docs/rfcs/0001-declarative-reconcile.md
Adds API reconciliation, dry-run and export support, CLI registration, tests, and documentation for MetaSchema.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 8e0dd

The new declarative metaschema flow can currently export configurations that reconciliation rejects and accept schemas that may fail during metadata validation, causing failed plans or applies and runtime validation errors. Merge should wait until export and runtime schema validation use consistent rules.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview Aug 14, 2026 7:12am

@coveralls

coveralls commented Aug 13, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 31779017944

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.2%) to 48.458%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 20 uncovered changes across 3 files (173 of 193 lines covered, 89.64%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
internal/reconcile/metaschema_reconciler.go 64 51 79.69%
internal/reconcile/metaschema.go 121 115 95.04%
cmd/reconcile.go 7 6 85.71%
Total (4 files) 193 173 89.64%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
cmd/reconcile.go 1 44.57%

Coverage Stats

Coverage Status
Relevant Lines: 39948
Covered Lines: 19358
Line Coverage: 48.46%
Coverage Strength: 15.54 hits per line

💛 - Coveralls

Comment thread docs/content/docs/reconcile.mdx
Comment thread internal/reconcile/metaschema_reconciler.go
Comment thread internal/reconcile/metaschema.go Outdated
Comment thread internal/reconcile/metaschema.go Outdated
Comment thread internal/reconcile/metaschema.go
@rohilsurana
rohilsurana marked this pull request as ready for review August 14, 2026 08:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e656a94-6d75-48b5-97c2-96e085fbc34c

📥 Commits

Reviewing files that changed from the base of the PR and between 229ea89 and 8e0dd53.

📒 Files selected for processing (15)
  • cmd/reconcile.go
  • core/metaschema/defaults.go
  • core/metaschema/defaults_test.go
  • core/metaschema/metaschemas/group.json
  • core/metaschema/metaschemas/org.json
  • core/metaschema/metaschemas/prospect.json
  • core/metaschema/metaschemas/role.json
  • core/metaschema/metaschemas/user.json
  • docs/content/docs/reconcile.mdx
  • docs/rfcs/0001-declarative-reconcile.md
  • internal/reconcile/metaschema.go
  • internal/reconcile/metaschema_reconciler.go
  • internal/reconcile/metaschema_reconciler_test.go
  • internal/reconcile/metaschema_test.go
  • internal/store/postgres/metaschema_repository.go

Comment on lines +88 to +96
func validateSchemaDocument(schema string) error {
root, err := decodeJSON(schema)
if err != nil {
return fmt.Errorf("schema is not valid JSON: %w", err)
}
if _, ok := root.(map[string]any); !ok {
return fmt.Errorf("schema must be a JSON object")
}
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

For the repository version of github.com/xeipuuv/gojsonschema, does validating data against the schema {"type":123} return a schema-compilation error, and can schema validation be performed without resolving remote $ref targets?

💡 Result:

For the github.com/xeipuuv/gojsonschema repository, the behavior for validating against the schema {"type": 123} is as follows: 1. Schema-compilation error: No, this does not trigger a schema-compilation error by default [1]. The library treats the schema as a valid JSON object. During validation, the "type" keyword expects a string or an array of strings (per JSON Schema specifications). Because 123 is a number, the library will fail to correctly process the "type" constraint during the validation phase, typically resulting in a validation result indicating the data does not match, rather than a hard failure during the NewSchema or Compile compilation step [1]. If you enable meta-schema validation via the SchemaLoader (setting sl.Validate = true), the library may detect the schema itself is invalid against the JSON Schema meta-schema, which would then return an error during the compilation/loading phase [1]. 2. Resolving remote $ref targets: No, the library does not provide a native configuration option to disable the automatic resolution of remote $ref targets [2]. The implementation automatically attempts to fetch remote references (e.g., via http/https) when encountered [3]. Users have reported that this behavior is "baked in" and difficult to bypass without external workarounds, such as pre-loading all required schemas using AddSchema/AddSchemas to satisfy dependencies before they are fetched over the network [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
git ls-files | grep -E '(^|/)(metaschema\.go|service\.go|go\.mod|go\.sum)$' || true

printf '%s\n' '--- relevant symbols and calls ---'
rg -n -C 5 'validateSchemaDocument|gojsonschema|SchemaLoader|Validate|exportMetaSchemas|MetaSchemaReconciler' internal core go.mod go.sum 2>/dev/null || true

printf '%s\n' '--- targeted source excerpts ---'
for f in internal/reconcile/metaschema.go core/metaschema/service.go go.mod; do
  if [ -f "$f" ]; then
    echo "### $f"
    nl -ba "$f" | sed -n '1,180p'
  fi
done

Repository: raystack/frontier

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -eu

for f in internal/reconcile/metaschema.go internal/reconcile/metaschema_reconciler.go core/metaschema/service.go core/metaschema/metaschema.go internal/reconcile/metaschema_test.go internal/reconcile/metaschema_reconciler_test.go go.mod; do
  if [ -f "$f" ]; then
    echo "### $f"
    wc -l "$f"
    nl -ba "$f" | sed -n '1,220p'
  fi
done

echo '### gojsonschema references'
rg -n -C 8 'gojsonschema|SchemaLoader|NewSchema|Validate\(' core internal --glob '*.go' | head -n 300

echo '### local module source availability'
go env GOMODCACHE GOPATH 2>/dev/null || true
find "${GOMODCACHE:-/nonexistent}" -maxdepth 5 -type d -path '*gojsonschema*' -print 2>/dev/null | head -n 20

Repository: raystack/frontier

Length of output: 26225


🏁 Script executed:

#!/bin/bash
set -eu

for spec in \
  'internal/reconcile/metaschema.go:1,210' \
  'internal/reconcile/metaschema_reconciler.go:1,112' \
  'core/metaschema/service.go:1,117' \
  'core/metaschema/metaschema.go:1,24' \
  'internal/reconcile/metaschema_test.go:1,251' \
  'internal/reconcile/metaschema_reconciler_test.go:1,146'; do
  file=${spec%:*}
  range=${spec#*:}
  echo "### $file"
  sed -n "${range}p" "$file"
done

echo '### direct gojsonschema imports and calls'
rg -n -C 10 'github.com/xeipuuv/gojsonschema|gojsonschema\.' --glob '*.go' . || true

mod=/home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0
echo "### gojsonschema v1.2.0 files"
find "$mod" -maxdepth 1 -type f -printf '%f\n' | sort

echo '### loader/compiler/validation implementation'
rg -n -C 12 'type SchemaLoader|Validate|NewSchema|Compile|validateSchema|typeSchema|remote|ReferenceLoader' "$mod" --glob '*.go' | head -n 500

Repository: raystack/frontier

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -eu

mod=/home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0

echo '### SchemaLoader and Validate defaults'
sed -n '1,240p' "$mod/schemaLoader.go"
sed -n '1,260p' "$mod/schema.go"

echo '### type keyword handling'
rg -n -C 15 'Type|type.*keyword|NewType|parseType|TypeError|expected.*type' "$mod" --glob '*.go' | head -n 300

echo '### remote reference loading'
sed -n '130,240p' "$mod/schemaPool.go"
rg -n -C 15 'func \(.*\) LoadJSON|http.Get|Get\(|NewStringLoader|NewSchemaLoader|Validate =' "$mod" --glob '*.go' | head -n 300

echo '### isolated runtime probe'
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
cat > "$tmp/go.mod" <<EOF
module isolated-probe

go 1.20

require github.com/xeipuuv/gojsonschema v1.2.0
EOF

cat > "$tmp/main.go" <<'EOF'
package main

import (
	"fmt"
	"github.com/xeipuuv/gojsonschema"
)

func main() {
	schemas := []string{
		`{"type":123}`,
		`{"type":"object"}`,
		`{"$ref":"https://127.0.0.1:1/unreachable.json"}`,
	}
	for _, raw := range schemas {
		result, err := gojsonschema.Validate(
			gojsonschema.NewStringLoader(raw),
			gojsonschema.NewGoLoader(map[string]any{}),
		)
		fmt.Printf("schema=%s\nerr=%v\n", raw, err)
		if result != nil {
			fmt.Printf("valid=%v\n", result.Valid())
		}
	}
}
EOF

(
  cd "$tmp"
  GOPROXY=off GOSUMDB=off go run .
)

Repository: raystack/frontier

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -u

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

cat > "$tmp/go.mod" <<'EOF'
module isolated-probe

go 1.20

require github.com/xeipuuv/gojsonschema v1.2.0
EOF

cat > "$tmp/main.go" <<'EOF'
package main

import (
	"fmt"

	"github.com/xeipuuv/gojsonschema"
)

func main() {
	schemas := []string{
		`{"type":123}`,
		`{"type":["object",123]}`,
		`{"required":123}`,
		`{"properties":123}`,
		`{"items":123}`,
		`{"minimum":"x"}`,
		`{"$ref":"https://127.0.0.1:1/unreachable.json"}`,
		`{"type":"object"}`,
	}

	for _, raw := range schemas {
		result, err := gojsonschema.Validate(
			gojsonschema.NewStringLoader(raw),
			gojsonschema.NewGoLoader(map[string]any{}),
		)
		fmt.Printf("schema=%s\nerr=%v\n", raw, err)
		if result != nil {
			fmt.Printf("valid=%v errors=%d\n", result.Valid(), len(result.Errors()))
		}
	}
}
EOF

cd "$tmp"
GOPROXY=off GOSUMDB=off GOFLAGS=-mod=mod go run . 2>&1

Repository: raystack/frontier

Length of output: 392


🏁 Script executed:

#!/bin/bash
set -u

echo '### cached dependency versions'
find /home/jailuser/go/pkg/mod/github.com/xeipuuv -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | sort
find /home/jailuser/go/pkg/mod/cache/download/github.com/xeipuuv -type f -name '*.mod' -o -name '*.ziphash' 2>/dev/null | sort | head -n 100

mod=/home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0

echo '### NewStringLoader and loader factories'
rg -n -C 12 'func NewStringLoader|type stringJSONLoader|LoaderFactory|func \(.*\) LoaderFactory' "$mod" --glob '*.go'

echo '### exact malformed-keyword branches'
rg -n -C 20 'KEY_TYPE|KEY_REQUIRED|KEY_PROPERTIES|KEY_ITEMS|KEY_MINIMUM|KEY_MAXIMUM' "$mod/schema.go" | head -n 500

echo '### compile a local-replacement probe when cached modules permit it'
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

cat > "$tmp/go.mod" <<'EOF'
module isolated-probe

go 1.20

require (
	github.com/xeipuuv/gojsonschema v1.2.0
	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb
	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415
)

replace github.com/xeipuuv/gojsonschema => /home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0
replace github.com/xeipuuv/gojsonpointer => /home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonpointer@v0.0.0-20190905194746-02993c407bfb
replace github.com/xeipuuv/gojsonreference => /home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonreference@v0.0.0-20180127040603-bd5ef7bd5415
EOF

cat > "$tmp/main.go" <<'EOF'
package main

import (
	"fmt"
	"github.com/xeipuuv/gojsonschema"
)

func main() {
	for _, raw := range []string{
		`{"type":123}`,
		`{"type":["object",123]}`,
		`{"required":123}`,
		`{"properties":123}`,
		`{"items":123}`,
		`{"minimum":"x"}`,
		`{"$ref":"https://127.0.0.1:1/unreachable.json"}`,
	} {
		result, err := gojsonschema.Validate(
			gojsonschema.NewStringLoader(raw),
			gojsonschema.NewGoLoader(map[string]any{}),
		)
		fmt.Printf("schema=%s\nerr=%v\n", raw, err)
		if result != nil {
			fmt.Printf("valid=%v errors=%d\n", result.Valid(), len(result.Errors()))
		}
	}
}
EOF

(cd "$tmp" && GOPROXY=off GOSUMDB=off GOFLAGS=-mod=mod go run .) 2>&1

Repository: raystack/frontier

Length of output: 44934


🏁 Script executed:

#!/bin/bash
set -eu

mod=/home/jailuser/go/pkg/mod/github.com/xeipuuv/gojsonschema@v1.2.0

echo '### public Validate wrapper'
rg -n -C 20 '^func Validate|^func \(.*\) Validate' "$mod" --glob '*.go'

echo '### HTTP reference loader'
sed -n '132,225p' "$mod/jsonLoader.go"

echo '### reference parsing and remote resolution'
sed -n '48,125p' "$mod/schemaPool.go"
sed -n '180,225p' "$mod/schemaPool.go"

Repository: raystack/frontier

Length of output: 11302


Reject schemas that gojsonschema cannot compile before planning updates.

validateSchemaDocument accepts {"type":123}, but gojsonschema.Validate returns a schema-compilation error for this input. Metadata validation can then fail for that entity type. Use the same gojsonschema compilation rules during reconcile. Define how remote $ref values are handled because NewStringLoader resolves uncached references over HTTP.

Comment on lines +196 to +201
curCanon, err := canonicalJSON(cur.Schema)
if err != nil {
// A stored schema that is not valid JSON still round-trips as its own
// literal, so emit it rather than dropping it.
specs = append(specs, MetaSchemaSpec{Name: name, Schema: cur.Schema})
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not export a spec that reconcile rejects.

When a stored schema is invalid JSON, this branch emits it verbatim. MetaSchemaReconciler.Validate rejects that exported value before reconcile starts. Therefore, frontier export metaschema can produce a document that its own reconcile flow cannot consume.

Return an export error with the metaschema name for invalid stored JSON, or define an exported representation that validation accepts.

Proposed fix
 		curCanon, err := canonicalJSON(cur.Schema)
 		if err != nil {
-			// A stored schema that is not valid JSON still round-trips as its own
-			// literal, so emit it rather than dropping it.
-			specs = append(specs, MetaSchemaSpec{Name: name, Schema: cur.Schema})
-			continue
+			return nil, fmt.Errorf("metaschema %q contains invalid JSON: %w", name, err)
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
curCanon, err := canonicalJSON(cur.Schema)
if err != nil {
// A stored schema that is not valid JSON still round-trips as its own
// literal, so emit it rather than dropping it.
specs = append(specs, MetaSchemaSpec{Name: name, Schema: cur.Schema})
continue
curCanon, err := canonicalJSON(cur.Schema)
if err != nil {
return nil, fmt.Errorf("metaschema %q contains invalid JSON: %w", name, err)
}

@rohilsurana
rohilsurana merged commit 059f974 into main Aug 14, 2026
8 checks passed
@rohilsurana
rohilsurana deleted the feat/metaschema-reconcile-kind branch August 14, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants