xRegistry Server Implementation Guide
August 18, 2026 · View on GitHub
⚠️ READ THE SPEC FIRST: https://github.com/xregistry/spec
The xRegistry specification is the authoritative source for API behavior, entity relationships, attribute definitions, and all protocol details. This document only supplements the spec with implementation-specific patterns and gotchas.
This document describes key patterns and design decisions in the xRegistry server. Some patterns are mandated by the xRegistry specification, while others are implementation-specific. Both are documented here for quick reference.
Purpose
Quick reference for developers working on this codebase, covering:
- Spec-mandated behaviors that might not be obvious from code alone
- Implementation-specific patterns unique to this MySQL-based server
- Database conventions and gotchas
- Testing patterns and requirements
When in doubt, consult the spec! This document exists to avoid constantly re-reading the spec for common patterns, but the spec always wins in case of conflict.
Entity Creation Patterns (from spec)
Implicit Parent/Child Creation
The xRegistry spec mandates that the server automatically creates parent and child entities when needed.
Creating versions auto-creates parents:
# This single request creates group "d1", resource "f1", AND version "v99"
PUT /dirs/d1/files/f1/versions/v99
{
"description": "My version"
}
The server will:
- Check if group "d1" exists; if not, create it
- Check if resource "f1" exists; if not, create it
- Create version "v99"
Creating resources auto-creates parents:
# Creates group "d1" and resource "f1"
# Also auto-creates version "1" since no version specified
PUT /dirs/d1/files/f1
{
"description": "My file"
}
Key points:
- ✅ Parents (groups, resources) are auto-created when creating children
- ✅ When creating a resource without specifying version, version "1" is auto-created
- ❌ Creating a version does NOT auto-create siblings (other versions)
Code Location: registry/resource.go - UpsertResource calls FindOrAddGroup; UpsertVersionWithObject handles implicit version creation
Why this matters: Tests don't need to explicitly create parent entities. One PUT can create the entire hierarchy.
Model Changes
As of now, removing a Group or Resource type will silently delete any of the corresponding entity instances of those types.
However, attribute-level changes that make any entity in the system be non-compliant will generate an error and the model update will be rejected.
Model Auto-Generation (from spec)
Document Attributes
When hasdocument=true (the spec default for resources), the following attributes become automatically available without being explicitly defined in the model:
<singular>- The document content itself (e.g.,file)<singular>url- External document URL (e.g.,fileurl)<singular>proxyurl- Proxied document URL (e.g.,fileproxyurl)<singular>base64- Base64-encoded document (e.g.,filebase64)
Example:
// Minimal model for a "files" resource:
{
"resources": {
"files": {
"singular": "file",
"plural": "files"
// hasdocument defaults to true
}
}
}
// Users can now provide these attributes in requests:
PUT /dirs/d1/files/f1/versions/1$details
{
"fileurl": "http://example.com/doc.txt" // Auto-available!
}
When hasdocument=false: These attributes are NOT auto-generated. Users can manually define attributes with these names in the model if needed, but they become regular user-defined attributes with no special document-handling behavior.
Code Location: registry/shared_model.go:1261-1267 - GetPropsOrdered() transforms $RESOURCE* template attributes based on hasdocument setting
Database Schema Conventions (implementation-specific)
PropName Includes Delimiter (DB_IN)
Critical Implementation Detail: In the Props table, the PropName column ALWAYS includes a trailing delimiter character (DB_IN, which is a comma ,).
Examples:
- Stored as:
"fileurl,"(note the comma) - NOT:
"fileurl"
Why? Simplifies parsing and prevents ambiguity (e.g., distinguishing "file," from "fileurl,").
When Querying Props:
-- CORRECT: Include DB_IN delimiter
WHERE PropName = 'fileurl' || string(DB_IN)
WHERE PropName IN ('file,', 'fileurl,', 'fileproxyurl,')
-- WRONG: Will never match any rows
WHERE PropName = 'fileurl'
WHERE PropName IN ('file', 'fileurl', 'fileproxyurl')
Code Locations:
common/const.go:50-DB_INconstant definition (const DB_IN = ',')registry/init.sql:196-217- Props table definition with detailed commentregistry/resource.go:1174-1209- Example of correct DB_IN usage in hasdocument violation check
This is the #1 source of bugs when writing new SQL queries. Always append DB_IN when filtering by PropName!
Entity Identifiers: SID vs UID
- SID (System ID): Auto-generated, globally unique. Used for all database JOINs and foreign keys.
- UID (User ID): User-provided from API (e.g., resource ID, version ID). Only unique within parent scope.
Code Location: Comment at top of registry/init.sql lines 11-22
Validation Order (implementation-specific)
When a single request updates both the model AND entity attributes (e.g., PUT / with modelsource attribute), the validation order matters.
Correct Flow
- Apply model changes (save to DB)
- Apply entity attribute changes
- Save entity changes
- Validate all data against the new model
Why This Matters
// Current state: description="OnE"
PUT /
{
"description": "TWO",
"modelsource": {
"attributes": {
"description": {
"enum": ["oNe", "TWO"]
}
}
}
}
- ❌ Wrong order: Apply model → Validate → Apply attributes = Fails (old "OnE" invalid under new enum)
- ✅ Right order: Apply model → Apply attributes → Validate = Succeeds (new "TWO" valid under new enum)
Implementation
Uses registry.SetStuff("modelchanged", true) to track when model changed, then calls VerifyData() after attribute updates instead of during model application.
Code Locations:
registry/info.go:1104-1111- Sets flag when model appliedregistry/registry.go:587-603- Checks flag and callsVerifyData()
Test Patterns (project conventions)
Byte-for-Byte Comparison
Tests compare exact JSON output, not wildcards.
// GOOD: Exact verification
XHTTP(t, reg, "PUT", "/dirs/d1", `{}`, 201, `{
"dirid": "d1",
"epoch": 1,
...
}`)
// AVOID: Hides regressions
XHTTP(t, reg, "PUT", "/dirs/d1", `{}`, 201, `*`)
Exceptions: Timestamps and hostnames are auto-masked by test framework.
Timestamp Masking Pattern
Test framework masks timestamps with pattern YYYY-MM-DDTHH:MM:SSZ where:
- Date/time portions (YYYY-MM-DDTHH:MM) are masked as literal strings
- Seconds (SS) start at
:01Zand increment for each operation::01Z,:02Z,:03Z, etc.
// First operation
"createdat": "YYYY-MM-DDTHH:MM:01Z",
"modifiedat": "YYYY-MM-DDTHH:MM:01Z"
// Second operation (PATCH)
"modifiedat": "YYYY-MM-DDTHH:MM:02Z" // Seconds increment
// Third operation
"modifiedat": "YYYY-MM-DDTHH:MM:03Z" // And so on
Key Points:
- Timestamps start at
:01Z(not:00Z) - Each write operation increments the seconds counter
- Read operations don't affect the counter
- Epoch can advance without timestamp changing if operations happen quickly
No Epoch Masks
Per user requirement: Always verify epoch increments correctly. Do NOT use epochMask to hide epoch values.
Code Location: tests/model3_test.go - Examples with explicit epoch values in expected output
Testing Resources with hasDocument=true
Resources with hasdocument=true have two access patterns:
- Document view: Direct resource URL returns the document content
- Metadata view: Use
$detailsendpoint to access xRegistry metadata
// WRONG: Can't PATCH metadata directly on hasDocument=true resource
XHTTP(t, reg, "PATCH", "/dirs/d1/files/f1", `{"description": "x"}`, ...)
// CORRECT: Use $details for metadata operations
XHTTP(t, reg, "PATCH", "/dirs/d1/files/f1$details", `{"description": "x"}`, ...)
// TESTING TIP: Set hasDocument=false in test models to simplify tests
_, err = gm.AddResourceModel("files", "file", 0, true, true, false) // false = hasDocument
Code Location: tests/capabilities_test.go - Examples using hasDocument=false for simpler tests
Quick Reference
Before Writing SQL Queries
- Appended
DB_INto PropName filters? - Used SID (not UID) for JOINs?
Before Changing Validation Logic
- Ensured model changes applied before validation?
- Tracked model changes if validation deferred?
Before Writing Tests
- Using byte-for-byte comparison (not wildcards)?
- Verifying epoch increments explicitly?
- Relying on implicit parent/child creation (no manual setup)?
Contributing
- Check this document first for patterns that aren't obvious from code
- Update this document when introducing new patterns
- Add code comments referencing this doc at critical decision points
- Verify spec compliance for API behavior changes
If code contradicts this document, the code is probably correct - update the document!