- 1. Architecture Overview
- 2. Principles & Constraints
- 3. Technical Architecture
- 4. Additional Context
- 5. Traceability
Simple Resource Registry follows a layered architecture with a pluggable storage backend. The module exposes a single REST API surface for CRUD operations on typed resources. All resources share a common schema envelope (identity, ownership, timestamps) and an opaque JSON payload. Schema envelope fields are queryable via OData; payload content is opaque and not queryable.
The resource type field contains the resource's GTS type identifier in GTS ID format. In other words, the resource type value is the same identifier as the resource's GTS type ID (GTS type ID).
The storage layer is abstracted behind a ResourceStoragePluginClient trait, allowing the same API to serve different backends. The default implementation is a relational backend (cpt-cf-srr-fr-default-storage-backend) described in the plugin DESIGN. The interface is intentionally minimal: the core module handles authentication, authorization, GTS type resolution, and event/audit emission, while backends handle persistence and query execution. Multiple backends can coexist with per-resource-type routing.
Resource type definitions are managed through GTS (Global Type System) via the Types Registry module. A base GTS schema defines the common envelope and behavioral flags (event/audit configuration). Derived types extend the base schema with type-specific payload definitions. This approach enables runtime discoverability of registered resource types and decouples type definition from storage implementation.
| Requirement | Design Response |
|---|---|
cpt-cf-srr-fr-idempotent-resource-create |
API layer extracts tenant/user from SecurityContext; domain layer assigns UUID and timestamps; delegates to storage plugin; ensure idempotency |
cpt-cf-srr-fr-read-resource |
ResourceService resolves permitted GTS types, then storage plugin applies backend-level filters (tenant, type scope, owner_id for per-user types) integrated with standard authz; returns 404 when no match is found |
cpt-cf-srr-fr-list-resources |
OData parser translates query params to backend query filters on schema columns; plugin executes scoped query |
cpt-cf-srr-fr-update-resource |
ResourceService resolves permitted GTS types, storage plugin fetches target resource with backend-level security filters, then domain layer validates payload and updates updated_at |
cpt-cf-srr-fr-delete-resource |
Soft-delete via deleted_at timestamp; storage plugin filters soft-deleted records from list queries |
cpt-cf-srr-fr-gts-type-registration |
Base GTS schema registered at module startup; derived types registered by consumer modules via Types Registry SDK |
cpt-cf-srr-fr-default-storage-backend |
Default relational DB backend implemented as a separate plugin module (srr-rdb-plugin); declares odata_support capability |
cpt-cf-srr-nfr-scalability |
100M resources / 100 writes·s⁻¹ / 1000 reads-by-ID·s⁻¹ — achieved via proper indexing in the default backend; storage optimization is a per-backend concern |
cpt-cf-srr-fr-odata-schema-fields |
OData $filter/$orderby limited to schema columns; cursor-based pagination (limit, cursor); payload column excluded from query support |
cpt-cf-srr-fr-gts-access-control |
ResourceService checks caller Permission entries (resource_pattern + action). Returns 403 for pre-query denials (POST, LIST no-intersection). For individual resources (GET/PUT/DELETE), permissions are enforced via backend query filters and unmatched scope results in 404 |
cpt-cf-srr-fr-gts-wildcard-filtering |
GET list accepts GTS wildcard in type filter (trailing *); uses GtsWildcard::new() + gts_id.wildcard_match() for matching; results intersected with caller's permitted GTS types |
cpt-cf-srr-fr-gts-type-validation |
ResourceService calls Types Registry SDK to verify GTS type existence; returns 400 gts-type-not-found if missing |
cpt-cf-srr-fr-deleted-retention |
Jobs Manager job permanently removes resources where deleted_at + deleted_resource_retention_days < now() for soft-deleted resources |
cpt-cf-srr-fr-notification-events |
Domain layer checks GTS type behavioral flags; emits fixed-schema events (id, type, subject_type, subject_id) via Events Broker SDK when enabled |
cpt-cf-srr-fr-audit-events |
Domain layer checks GTS type audit flags (including inherited flags from parent GTS types); emits fixed-schema audit events (id, type, subject_type, subject_id, previous_payload, new_payload) via Audit SDK when enabled |
cpt-cf-srr-fr-multi-backend-storage |
ResourceStoragePluginClient trait enables additional backends including vendor-provided plugins for existing platform storage; GTS-based plugin discovery and scoped ClientHub clients per ModKit plugin pattern |
cpt-cf-srr-fr-storage-routing |
Configuration maps GTS type IDs to storage plugin instances; router resolves plugin per request |
cpt-cf-srr-fr-batch-operations |
Batch endpoints (POST /resources:batch, POST /resources:batch-get) delegate per-item to ResourceService with shared SecurityContext; 207 Multi-Status with per-item results per DNA BATCH.md; configurable batch size cap |
cpt-cf-srr-fr-resource-groups |
Many-to-many group membership model; group-level batch operations (list, delete); group lifecycle |
cpt-cf-srr-fr-idempotent-resource-create |
POST requires idempotency_key; plugin atomically checks and persists the key+resource in one transaction; CreateOutcome::Duplicate returned for duplicate key within retention window → 409 with existing resource id |
| ADR | Decision Summary |
|---|---|
cpt-cf-srr-adr-plugin-storage |
Plugin-based multi-backend storage architecture — trait-based plugin interface with GTS-based discovery; enables vendor extensibility and multi-backend coexistence |
cpt-cf-srr-adr-db-security-filtering |
DB-level security filtering — tenant, owner, and GTS type scope enforced as query predicates (404 for filtered-out resources); 403 reserved for pre-query type-scope denials |
cpt-cf-srr-adr-search-strategy |
Search is backend capability-driven (search_support) and enabled per resource type via routing; 501 when unsupported |
| NFR ID | NFR Summary | Allocated To | Design Response | Verification Approach |
|---|---|---|---|---|
cpt-cf-srr-nfr-read-latency |
Single-resource GET < 50ms p95 | Storage Backend, Database | ID-based lookup with tenant scoping; backend-internal indexing; connection pooling; ResourceTypeConfig (behavioral flags + retention) cached in-process using Arc<RwLock<HashMap<String, ResourceTypeConfig>>> to avoid repeated Types Registry SDK round-trips per request |
Load test with p95 measurement |
cpt-cf-srr-nfr-payload-size |
Payload ≤ 64 KB | API Layer | Request body size validation before deserialization | Unit test with oversized payloads |
cpt-cf-srr-nfr-scalability |
100M resources; 100 writes/s; 1000 reads-by-ID/s | Storage Backend | Proper indexing on schema fields; storage optimization is backend-internal | Load test with sustained throughput measurement |
┌──────────────────────────────────────────────────────────┐
│ API Layer │
│ REST endpoints, OpenAPI, request validation │
├──────────────────────────────────────────────────────────┤
│ Application Layer │
│ ResourceService: orchestration, auth, events │
├──────────────────────────────────────────────────────────┤
│ Domain Layer │
│ Resource entity, GTS type resolution, │
│ behavioral flags, storage plugin trait │
├──────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ Default Storage Plugin │ Future plugins │
│ Events Broker SDK │ Audit SDK │ Types Registry │
└──────────────────────────────────────────────────────────┘
| Layer | Responsibility | Technology |
|---|---|---|
| API | REST endpoints, request/response serialization, OData query parsing, OpenAPI spec | modkit REST, OpenAPI 3.0 |
| Application | Request orchestration, SecurityContext propagation, GTS type resolution, event/audit emission | Rust async services |
| Domain | Resource entity model, storage plugin trait definition, behavioral flag evaluation | Rust traits, GTS schemas |
| Infrastructure | Storage implementations, external module SDK clients | Storage Plugin(s), Events Broker SDK, Audit SDK, Types Registry SDK |
-
p1- ID:cpt-cf-srr-principle-schema-payload-separation
All resources share fixed schema columns (id, tenant_id, owner_id, type, timestamps) and an opaque payload column. Schema columns are indexed and queryable via OData. Payload is stored and returned as-is without indexing or query support.
This principle keeps the storage model simple and avoids per-type table proliferation while still enabling meaningful filtering on structural fields.
-
p1- ID:cpt-cf-srr-principle-plugin-isolation
ADRs: cpt-cf-srr-adr-plugin-storage
Storage plugins implement only persistence and query execution. They receive already-validated, already-authorized requests from the domain layer. Plugins do not perform authentication, authorization, GTS type resolution, or event emission. This keeps plugin implementations thin and ensures security logic is centralized. The trait is designed so that platform vendors can implement custom plugins to bridge the registry API to existing platform storage components, allowing the registry to act as a unified façade without requiring data migration.
-
p1- ID:cpt-cf-srr-principle-gts-first
All resource types are defined and registered through GTS. The base resource type and derived types use GTS schema inheritance. Type behavioral flags (event/audit configuration) are stored in the GTS type definition, not in module configuration. This ensures type metadata is discoverable, versionable, and consistent across the platform.
-
p1- ID:cpt-cf-srr-constraint-backend-internal-storage
Storage layout and optimization (table design, indexing, partitioning) are backend-internal concerns. The main module defines a canonical reference schema (see §3.8) as the recommended starting point for relational backends — this ensures predictable performance, easier plugin implementations, and consistent migrations. Non-relational backends may use any internal data structure appropriate to their storage technology. For resource types requiring specialized storage (full-text search, dedicated indexing), per-resource-type routing (cpt-cf-srr-fr-multi-backend-storage) enables alternative backends.
-
p1- ID:cpt-cf-srr-constraint-no-payload-query
OData queries cannot target payload fields. Consumers requiring payload-level queries must either promote frequently queried fields to the schema level (by extending the module) or use search-capable storage backends (cpt-cf-srr-fr-search-api). This constraint is a deliberate trade-off for simplicity and predictable performance.
-
p1- ID:cpt-cf-srr-constraint-soft-delete
Resources are soft-deleted via the API (deleted_at timestamp set). Soft-deleted resources are permanently purged by a Jobs Manager job after a configurable retention period (default 30 days, overridable per resource type via GTS type definition). The module does not support automatic soft-deletion of active resources based on a TTL — this is intentionally excluded as error-prone.
-
p1- ID:cpt-cf-srr-constraint-payload-size
Resource payloads are limited to 64 KB. This prevents storage abuse and ensures predictable query performance. Consumers needing to store large binary data should use File Storage and reference files by URL in the payload.
Technology: GTS (JSON Schema), Rust structs
Core Entities:
| Entity | Description |
|---|---|
| Resource | A stored object with schema envelope + JSON payload |
| ResourceType | GTS type definition containing behavioral flags and payload schema |
| BaseResourceType | The GTS base schema shared by all resource types |
Base Resource Type GTS Schema (major-version-only):
Behavioral flags (event/audit configuration, user-scoping, retention) are defined as GTS schema traits via x-gts-traits-schema per GTS spec §9.7. Traits are not part of the instance data model — they configure cross-cutting system behavior. Derived resource types provide concrete trait values via x-gts-traits.
{
"$id": "gts://gts.cf.core.srr.resource.v1~",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SimpleResource",
"type": "object",
"x-gts-traits-schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"is_per_owner_resource": {
"type": "boolean",
"default": false,
"description": "When true, resources of this type are user-scoped (owner_id is set from SecurityContext)."
},
"is_create_event_needed": {
"type": "boolean",
"default": false,
"description": "Emit a notification event on resource creation."
},
"is_update_event_needed": {
"type": "boolean",
"default": false,
"description": "Emit a notification event on resource update."
},
"is_delete_event_needed": {
"type": "boolean",
"default": false,
"description": "Emit a notification event on resource deletion."
},
"is_create_audit_event_needed": {
"type": "boolean",
"default": false,
"description": "Emit an audit event on resource creation."
},
"is_update_audit_event_needed": {
"type": "boolean",
"default": false,
"description": "Emit an audit event on resource update."
},
"is_delete_audit_event_needed": {
"type": "boolean",
"default": false,
"description": "Emit an audit event on resource deletion."
},
"deleted_resource_retention_days": {
"type": ["integer", "null"],
"minimum": 0,
"default": null,
"description": "Days to retain soft-deleted resources before purge. null = system default (30 days). 0 = immediate hard-delete."
}
}
},
"properties": {
"id": { "type": "string", "format": "uuid" },
"type": { "type": "string" },
"tenant_id": { "type": "string", "format": "uuid" },
"owner_id": { "type": ["string", "null"], "format": "uuid" },
"created_at": { "type": "string", "format": "date-time" },
"updated_at": { "type": "string", "format": "date-time" },
"deleted_at": { "type": ["string", "null"], "format": "date-time" },
"payload": {
"type": "object",
"description": "The resource payload. Schema is defined by the derived GTS type."
}
},
"additionalProperties": false,
"required": ["id", "type", "tenant_id"]
}Example Derived Resource Type (with x-gts-traits providing concrete trait values):
{
"$id": "gts://gts.cf.core.srr.resource.v1~acme.crm._.contact.v1~",
"allOf": [
{ "$ref": "gts://gts.cf.core.srr.resource.v1~" },
{
"x-gts-traits": {
"is_per_owner_resource": false,
"is_create_event_needed": true,
"is_update_event_needed": true,
"is_delete_event_needed": true,
"is_create_audit_event_needed": true,
"is_update_audit_event_needed": true,
"is_delete_audit_event_needed": false,
"deleted_resource_retention_days": 90
},
"properties": {
"payload": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string", "format": "email" }
},
"required": ["name"]
}
}
}
]
}GTS Schema Struct (in SDK crate, generates the base GTS JSON Schema at compile time):
use gts_macros::struct_to_gts_schema;
/// Base GTS schema for Simple Resource Registry resources.
/// Derived resource types extend this base via GTS schema inheritance
/// and provide concrete trait values (behavioral flags, retention config)
/// via `x-gts-traits`.
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.cf.core.srr.resource.v1~",
description = "Base schema for Simple Resource Registry resources",
properties = "id,type,tenant_id,owner_id,created_at,updated_at,deleted_at,payload"
)]
pub struct BaseSimpleResourceV1 {
pub id: Uuid,
pub r#type: String,
pub tenant_id: Uuid,
pub owner_id: Option<Uuid>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub deleted_at: Option<DateTimeUtc>,
pub payload: serde_json::Value,
}Rust Domain Structs (conceptual, in domain/ layer with #[domain_model]):
use modkit_macros::domain_model;
/// Schema envelope stored as dedicated DB columns.
/// Internal domain representation — not the GTS schema definition.
#[domain_model]
pub struct Resource {
pub id: Uuid,
pub r#type: String,
pub tenant_id: Uuid,
pub owner_id: Option<Uuid>,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub deleted_at: Option<DateTimeUtc>,
pub payload: serde_json::Value,
}
/// Resolved effective GTS traits for a resource type.
/// Built by merging `x-gts-traits` values along the GTS inheritance chain
/// per GTS spec §9.7.5. Audit flags are effective if enabled on the type
/// itself or any parent GTS type.
#[domain_model]
pub struct ResourceTypeConfig {
pub r#type: String,
pub is_per_owner_resource: bool,
pub is_create_event_needed: bool,
pub is_delete_event_needed: bool,
pub is_update_event_needed: bool,
pub is_create_audit_event_needed: bool,
pub is_update_audit_event_needed: bool,
pub is_delete_audit_event_needed: bool,
/// Days to retain soft-deleted resources. None = system default (30 days). 0 = immediate.
pub deleted_resource_retention_days: Option<u32>,
}Relationships:
- Resource → ResourceType: Each resource references a GTS type via type
- ResourceType → BaseResourceType: Derived types extend the base type via GTS inheritance
- Resource → Tenant: Every resource belongs to exactly one tenant (mandatory)
- Resource → User: Resources optionally belong to a user (when is_per_owner_resource=true)
graph TB
Consumer[Consumer / REST Client] --> API[API Layer]
SDK[SDK Client via ClientHub] --> SVC[ResourceService]
API --> SVC
SVC --> ACL[GTS Access Control]
ACL --> TR[Types Registry SDK]
SVC --> Cache[GTS Type Cache]
Cache --> TR
SVC --> Router[Storage Router]
Router --> DefaultPlugin[Default Storage Plugin]
Router --> ES[Search Plugin]
SVC --> EB[Events Broker SDK]
SVC --> AU[Audit SDK]
DefaultPlugin --> DB[(Database)]
RetentionJob[Retention Purge Jobs Manager job] --> Router
RetentionJob --> TR
Components:
-
p1- ID:cpt-cf-srr-component-api-layer- API Layer: REST endpoint handlers, request/response DTOs, OData query parsing, OpenAPI spec generation. Implements
cpt-cf-srr-interface-rest-api. Extracts SecurityContext and delegates to ResourceService.
- API Layer: REST endpoint handlers, request/response DTOs, OData query parsing, OpenAPI spec generation. Implements
-
p1- ID:cpt-cf-srr-component-resource-service- ResourceService: Core application service. Delegates to GTS Access Control for permission checks and effective type scope resolution, validates GTS type existence via Types Registry (with in-process GTS Type Cache for performance), applies DB-filter-based access semantics (tenant, user, type), evaluates behavioral flags (including flags inherited from parent GTS types), orchestrates storage plugin calls, emits events (
cpt-cf-srr-contract-events) and audit (cpt-cf-srr-contract-audit) percpt-cf-srr-fr-notification-eventsandcpt-cf-srr-fr-audit-events. Exposed to other modules via ClientHub asSimpleResourceRegistryClient(cpt-cf-srr-interface-sdk-client) through aLocalClientadapter (per ModKit DDD-light pattern).
- ResourceService: Core application service. Delegates to GTS Access Control for permission checks and effective type scope resolution, validates GTS type existence via Types Registry (with in-process GTS Type Cache for performance), applies DB-filter-based access semantics (tenant, user, type), evaluates behavioral flags (including flags inherited from parent GTS types), orchestrates storage plugin calls, emits events (
-
p1- ID:cpt-cf-srr-component-gts-access-control- GTS Access Control: Checks the caller's
SecurityContext.permissions()forPermissionentries whoseresource_patternmatches GTS types (using GTS wildcard matching) and whoseactionmatches the operation (read/create/update/delete). For POST, validates requested type is allowed. For list queries, computes the intersection of requested pattern and permitted patterns. For individual resource operations, returns effective permitted type filters used by DB queries.
- GTS Access Control: Checks the caller's
-
p1- ID:cpt-cf-srr-component-gts-type-cache- GTS Type Cache: In-process cache of resolved
ResourceTypeConfigvalues (behavioral flags + retention config) keyed by GTS type ID string. Implemented asArc<RwLock<HashMap<String, ResourceTypeConfig>>>following thegts-rustin-memory registry pattern. Cache is populated on first access and used before calling the Types Registry SDK.
- GTS Type Cache: In-process cache of resolved
-
p1- ID:cpt-cf-srr-component-storage-router- Storage Router: Resolves the correct storage plugin for a given resource type based on the
cpt-cf-srr-fr-storage-routingconfiguration. For each request, the router matches the resource GTS type ID against configured type ID patterns and returns the associated plugin instance. When no per-type rule matches, the router falls back to the default Relational Database plugin (cpt-cf-srr-component-relational-db-plugin).
- Storage Router: Resolves the correct storage plugin for a given resource type based on the
-
p1- ID:cpt-cf-srr-component-relational-db-plugin- Relational Database Backend (
relational-db): Default implementation ofResourceStoragePluginClient. Registers as a scoped client in ClientHub with its GTS instance ID. Declaresodata_support: true,search_support: false. See plugin DESIGN for implementation details.
- Relational Database Backend (
-
p3- ID:cpt-cf-srr-component-retention-purge-task- Retention Purge Jobs Manager job: Jobs Manager job that periodically scans for soft-deleted resources whose
deleted_atexceeds their type'sdeleted_resource_retention_daysand permanently hard-deletes them from storage. Uses the system default (30 days) when no type-level override is specified.
- Retention Purge Jobs Manager job: Jobs Manager job that periodically scans for soft-deleted resources whose
-
p4- ID:cpt-cf-srr-component-search-plugin- Search Plugin: Future implementation of
ResourceStoragePluginClientfor search-heavy resource types (e.g., ElasticSearch, OpenSearch). Provides full-text search within payload fields. Declaressearch_supportcapability.
- Search Plugin: Future implementation of
Plugin Architecture (per MODKIT_PLUGINS.md):
The storage plugin system follows the standard ModKit plugin pattern:
- SDK crate (
simple-resource-registry-sdk): Defines the publicSimpleResourceRegistryClienttrait (cpt-cf-srr-interface-sdk-client), theResourceStoragePluginClientbackend trait (cpt-cf-srr-interface-storage-backend), and theResourceStoragePluginSpecV1GTS schema type for backend instances. - Main module (
simple-resource-registry): Registers the plugin GTS schema in Types Registry duringinit(), resolves plugins via GTS-based discovery, and registers a public client in ClientHub. - Plugin modules (e.g.,
srr-rdb-plugin): Each plugin registers a GTS instance in Types Registry and a scoped client in ClientHub viaClientScope::gts_id(&instance_id).
Plugin discovery uses the standard pattern:
GTS plugin instance ID: gts.cf.core.modkit.plugin.v1~x.cf.simple_resource_registry.plugin.v1~<vendor>.<plugin_name>._.plugin.v1
Plugin API Trait (defined in SDK crate):
/// Plugin capabilities that determine which API operations are supported.
/// Declared by each plugin to enable capability-aware routing at the API layer.
pub struct PluginCapabilities {
pub odata_support: bool,
pub search_support: bool,
}
/// Result of a create operation — distinguishes a newly created resource from a duplicate.
pub enum CreateOutcome {
/// Resource was newly created and persisted.
Created(Resource),
/// A resource with this idempotency_key already exists for this tenant (and owner, for per-owner types); returns its id.
Duplicate(Uuid),
}
/// Plugin API — implemented by plugin modules, called by the main module.
/// Registered WITH scope (GTS instance ID) in ClientHub.
#[async_trait]
pub trait ResourceStoragePluginClient: Send + Sync {
/// Return the capabilities supported by this plugin
fn capabilities(&self) -> PluginCapabilities;
/// Persist a new resource and atomically record the idempotency key.
///
/// **Idempotency contract** (all plugins MUST comply):
/// - Key scope: `(tenant_id, owner_id, idempotency_key)` — for per-owner types (`is_per_owner_resource=true`),
/// owner_id is set from `SecurityContext.subject_id`; for non-per-owner types, a nil UUID sentinel
/// is used so the constraint reduces to `(tenant_id, idempotency_key)` in effect
/// - Dedup window: default 24 h from creation (configurable per deployment)
/// - Atomicity: check + insert resource + insert idempotency record MUST be
/// atomic (single DB transaction or equivalent)
/// - Returns `CreateOutcome::Duplicate(existing_resource_id)` if key exists within window
/// - Returns `CreateOutcome::Created(resource)` when no match exists
/// - After TTL expiry, the key is no longer enforced; expired records are
/// purged by a background job
///
/// All plugins MUST pass the SDK idempotency conformance test suite.
async fn create(&self, ctx: &SecurityContext, resource: &Resource, idempotency_key: &str) -> Result<CreateOutcome>;
/// Get a single resource by ID
async fn get(&self, ctx: &SecurityContext, id: Uuid) -> Result<Option<Resource>>;
/// List resources with OData query parameters.
/// Only called if capabilities().odata_support == true.
/// Plugins declaring odata_support MUST support $filter/$orderby on all
/// canonical schema fields defined in the OData Query Grammar (§3.3).
async fn list(
&self,
ctx: &SecurityContext,
r#type: &str,
odata: &ODataQuery,
) -> Result<PagedResult<Resource>>;
/// Search resources with full-text query.
/// Only called if capabilities().search_support == true.
async fn search(
&self,
ctx: &SecurityContext,
r#type: &str,
query: &SearchQuery,
) -> Result<PagedResult<Resource>>;
/// Update an existing resource
async fn update(&self, ctx: &SecurityContext, resource: &Resource) -> Result<Resource>;
/// Soft-delete a resource by setting deleted_at = now()
async fn delete(&self, ctx: &SecurityContext, id: Uuid) -> Result<()>;
/// Immediately hard-delete a single resource by ID (used when retention_days == 0).
async fn hard_delete(&self, ctx: &SecurityContext, id: Uuid) -> Result<()>;
/// Hard-delete resources that were soft-deleted before the cutoff timestamp.
/// Used by the retention purge Jobs Manager job.
async fn purge_deleted_before(
&self,
r#type: &str,
cutoff: DateTimeUtc,
batch_size: u32,
) -> Result<u64>;
}Public API Trait (defined in SDK crate):
/// Public API — exposed by the module to other modules.
/// Registered WITHOUT scope in ClientHub.
#[async_trait]
pub trait SimpleResourceRegistryClient: Send + Sync {
async fn create_resource(&self, ctx: &SecurityContext, r#type: &str, idempotency_key: &str, payload: Value) -> Result<Resource, SrrError>;
async fn get_resource(&self, ctx: &SecurityContext, id: Uuid) -> Result<Resource, SrrError>;
async fn list_resources(&self, ctx: &SecurityContext, r#type: &str, odata: &ODataQuery) -> Result<PagedResult<Resource>, SrrError>;
async fn update_resource(&self, ctx: &SecurityContext, id: Uuid, payload: Value) -> Result<Resource, SrrError>;
async fn delete_resource(&self, ctx: &SecurityContext, id: Uuid) -> Result<(), SrrError>;
}Plugin Resolution (in main module ResourceService):
// Storage Router resolves plugin per resource type via GTS-based discovery:
// 1. Looks up routing config for type
// 2. Resolves the target plugin's GTS instance ID
// 3. Gets the scoped client: hub.get_scoped::<dyn ResourceStoragePluginClient>(&scope)Technology: REST/OpenAPI 3.0
Base Path: /simple-resource-registry/v1
Endpoints Overview:
| Method | Path | Description | FR | Stability |
|---|---|---|---|---|
POST |
/resources |
Create a new resource | cpt-cf-srr-fr-idempotent-resource-create |
stable |
GET |
/resources/{id} |
Get a single resource by ID | cpt-cf-srr-fr-read-resource |
stable |
GET |
/resources |
List resources (OData filtering) | cpt-cf-srr-fr-list-resources |
stable |
PUT |
/resources/{id} |
Update a resource | cpt-cf-srr-fr-update-resource |
stable |
DELETE |
/resources/{id} |
Soft-delete a resource | cpt-cf-srr-fr-delete-resource |
stable |
POST |
/resources:batch |
Batch CRUD operations (create/update/delete) | cpt-cf-srr-fr-batch-operations |
stable |
POST |
/resources:batch-get |
Batch get resources by IDs | cpt-cf-srr-fr-batch-operations |
stable |
POST |
/resources:search |
Full-text search resources | cpt-cf-srr-fr-search-api |
experimental |
Request/Response Schemas:
Request Body:
{
"id": "uuid (optional, caller-supplied; system-generated if omitted)",
"type": "string (GTS type ID of the derived resource type)",
"idempotency_key": "string (required, caller-supplied deduplication UUID)",
"payload": { }
}Response (201 Created):
{
"id": "uuid",
"type": "string",
"tenant_id": "uuid",
"owner_id": "uuid | null",
"created_at": "datetime",
"updated_at": "datetime",
"deleted_at": null,
"payload": { }
}Notes:
tenant_idis set fromSecurityContext.subject_tenant_id(Subject Owner Tenant)owner_idis set fromSecurityContext.subject_id(only whenis_per_owner_resource=true)idis optional in the request; if omitted, the system assigns a generated UUID. If provided, it must be a valid UUIDcreated_at,updated_atare system-generated201 Createdresponse includesLocation: /simple-resource-registry/v1/resources/{id}idempotency_keyis required; a second POST with the same key within the same tenant (and same owner for per-owner types) returns 409 with the existing resourceidinstead of creating a duplicate- Idempotency keys are scoped to
(tenant_id, owner_id, idempotency_key). For per-owner resource types (is_per_owner_resource=true),owner_idis set fromSecurityContext.subject_id; for non-per-owner types, a nil UUID is used so the scope effectively reduces to(tenant_id, idempotency_key). The same key in different tenants is treated as distinct - The plugin atomically records the idempotency key alongside resource creation per the SDK idempotency contract (see §3.2
createmethod): key scope(tenant_id, owner_id, idempotency_key), default 24 h dedup window, atomic check-and-insert
Response (200 OK):
{
"id": "uuid",
"type": "string",
"tenant_id": "uuid",
"owner_id": "uuid | null",
"created_at": "datetime",
"updated_at": "datetime",
"deleted_at": null,
"payload": { }
}Query Parameters:
$filter— OData filter on schema fields (e.g.,type eq 'my-type' and owner_id eq '...'). Supports GTS wildcard matching ontypewith trailing*per GTS spec (e.g.,type eq 'gts.cf.core.srr.resource.v1~acme.*')$orderby— OData ordering on schema fields (e.g.,created_at desc)limit— Maximum number of results (default: 50, max: 1000)cursor— Opaque, versioned cursor for cursor-based pagination$select— Not supported. All schema fields and payload are always returned. Field projection is not applicable because the payload is an opaque JSON object whose structure varies by resource type.
Response (200 OK):
{
"items": [
{
"id": "uuid",
"type": "string",
"tenant_id": "uuid",
"owner_id": "uuid | null",
"created_at": "datetime",
"updated_at": "datetime",
"deleted_at": null,
"payload": { }
}
],
"page_info": {
"limit": 20,
"next_cursor": "opaque-cursor-string | null",
"prev_cursor": "opaque-cursor-string | null"
}
}Notes:
- Results are always scoped to the caller's tenant (enforced by the storage backend)
- Soft-deleted resources are always excluded from list and get results; there is no API mechanism to retrieve soft-deleted resources (the
deleted_atfield is not exposed as a user-facing filter). Soft-deleted resources are only accessible via the retention purge Jobs Manager job. - Filtering on
payloadfields is not supported and returns 400 Bad Request - When
typefilter uses a wildcard (trailing*), results are intersected with the caller's permitted GTS types from token permissions - GTS wildcard must appear only once, at the end, per GTS spec section 10
OData Query Grammar (cpt-cf-srr-fr-odata-schema-fields):
The OData subset is restricted to an explicit allowlist of schema fields and operators per DNA QUERYING.md. The ODataFilterable derive macro enforces the allowlist at compile time; payload fields are unconditionally rejected.
Allowed $filter fields:
| Field | Operators | Type | Notes |
|---|---|---|---|
type |
eq |
String | Supports GTS trailing * wildcard for prefix matching |
owner_id |
eq |
UUID | |
created_at |
eq, gt, ge, lt, le |
DateTimeUtc | |
updated_at |
eq, gt, ge, lt, le |
DateTimeUtc | |
id |
eq, in |
UUID | in capped at 50 values |
Allowed $orderby fields:
| Field | Directions |
|---|---|
created_at |
asc, desc |
updated_at |
asc, desc |
id |
asc, desc (always appended as tiebreaker per DNA QUERYING.md) |
Query limits:
| Limit | Value |
|---|---|
Maximum filter predicates per $filter |
5 |
Maximum page size (limit) |
1000 |
| Default page size | 50 |
Nested or groups |
Forbidden |
Payload field references in $filter/$orderby |
Forbidden → 400 invalid-odata-query |
All plugins declaring odata_support: true MUST support $filter and $orderby on the canonical schema fields listed above. This ensures consistent query behavior regardless of the storage backend.
Access Control Model (applies to GET, LIST, UPDATE, DELETE):
All access checks are applied as backend query filters, not as post-fetch in-memory checks. The storage plugin query always includes:
tenant_id = ctx.subject_tenant_id(subject_tenant_idis the Subject Owner Tenant fromSecurityContext)type IN (permitted GTS types from token claims)owner_id = ctx.subject_id(only whenis_per_owner_resource=true;subject_idis the authenticated subject identifier fromSecurityContext)- Exclude soft-deleted resources
This means 403 is never returned for individual resource operations (GET, UPDATE, DELETE) — if the caller lacks access, the resource is simply not found and the system returns 404. The caller cannot distinguish between "does not exist" and "not authorized." 403 is only returned in two cases: (1) on POST when the resource type is not in the caller's GTS token scope (pre-query check), and (2) on LIST when the requested type filter has zero intersection with the caller's permitted GTS types.
Request Body:
{
"payload": { }
}Response (200 OK): Full resource object with updated payload and updated_at.
Notes:
- Only
payloadcan be updated; schema fields (type, tenant_id, owner_id) are immutable after creation updated_atis automatically set
Response (204 No Content)
Notes:
- Sets
deleted_atto current timestamp - Resource remains in storage but is excluded from list queries
Error Responses (RFC 9457 Problem Details):
All error responses use the RFC 9457 Problem Details format (application/problem+json). Problem Details type is a stable error URI; table entries below use short slugs for readability.
| HTTP Status | Problem type slug | Description |
|---|---|---|
| 400 | invalid-gts-wildcard |
GTS wildcard pattern in $filter is malformed (e.g., * not at end, multiple wildcards) |
| 400 | invalid-odata-query |
OData query references payload fields or has invalid syntax |
| 400 | payload-too-large |
Payload exceeds 64 KB limit |
| 400 | batch-size-exceeded |
Batch request exceeds the maximum allowed items (default: 100) |
| 400 | gts-type-not-found |
Referenced resource type (GTS type ID / GTS type ID) does not exist in Types Registry. Response includes the unresolved GTS type ID |
| 401 | unauthenticated |
Missing or invalid authentication |
| 403 | forbidden |
Caller lacks general permission for this operation |
| 403 | gts-type-not-in-scope |
Caller's token permissions do not include a matching GTS resource_pattern for the target resource type. Returned as a pre-query check on POST, or when no permitted types match on LIST. Extensions include the required GTS type and action |
| 404 | not-found |
Resource does not exist, or was filtered out by backend-level security filters (tenant scope, owner_id scope for per-user resources, GTS type scope). Because all access checks are applied as backend query filters, a mismatch on any dimension results in 404 — the caller cannot distinguish between "does not exist" and "not authorized" for individual resources |
| 409 | duplicate-idempotency-key |
A resource was already successfully created with the same idempotency_key within the same tenant. Response body includes the id of the existing resource |
| 422 | validation-error |
On POST: resource type requires owner_id (is_per_owner_resource=true) but none available in SecurityContext. On POST/PUT: payload fails GTS schema validation. Other semantic validation failures |
| 501 | search-not-supported |
The storage plugin for this resource type does not support search operations |
Example Error Response (403):
{
"type": "gts.cf.core.errors.err.v1~cf.core.http.forbidden.v1~",
"title": "GTS type not in scope",
"status": 403,
"detail": "Caller lacks 'create' permission for GTS type 'gts.cf.core.srr.resource.v1~acme.widget'",
"instance": "/simple-resource-registry/v1/resources",
"trace_id": "01J..."
}Request Body:
{
"type": "string (GTS type ID or wildcard pattern)",
"query": "string (full-text search query)",
"filters": {
"owner_id": "uuid (optional)",
"created_after": "datetime (optional)",
"created_before": "datetime (optional)"
},
"limit": 50,
"cursor": "opaque-cursor-string (optional)"
}Response (200 OK):
{
"items": [
{ "...full resource object..." }
],
"page_info": {
"limit": 50,
"next_cursor": "opaque-cursor-string | null",
"prev_cursor": null
}
}Response (501 Not Implemented — RFC 9457 Problem Details):
{
"type": "gts.cf.core.errors.err.v1~cf.core.http.not_implemented.v1~",
"title": "Search not supported by plugin",
"status": 501,
"detail": "The storage plugin for resource type 'gts.cf.core.srr.resource.v1~acme.widget' does not support search operations",
"trace_id": "01J..."
}Notes:
- Tenant scoping is automatically applied from SecurityContext (not a request field)
- The API checks the target plugin's
search_supportcapability before routing - If the plugin lacks
search_support, returns 501 Not Implemented - GTS type-based access control applies (same as GET list)
- Full-text search is performed within resource payloads
Request Body:
{
"items": [
{ "data": { "id": "uuid" } },
{ "data": { "id": "uuid" } }
]
}Response (207 Multi-Status):
{
"items": [
{ "index": 0, "status": 200, "data": { "...full resource object..." } },
{ "index": 1, "status": 404, "error": { "type": "gts.cf.core.errors.err.v1~cf.core.http.not_found.v1~", "title": "Resource not found", "status": 404, "detail": "Resource with id '01J...' does not exist", "trace_id": "01J...-item-1" } }
]
}Notes:
- Top-level HTTP status follows DNA aggregate rules: all succeeded →
200; partial →207; all failed same type → matching4xx; all failed mixed →207 - Each item is independently resolved with tenant scoping and GTS access control (best-effort semantics)
- Items that fail access control or do not exist return per-item errors (RFC 9457 Problem Details)
- Maximum batch size: configurable (default 100)
Request Body (create example):
{
"items": [
{
"idempotency_key": "req-1-uuid",
"data": { "action": "create", "type": "string", "payload": { } }
},
{
"idempotency_key": "req-2-uuid",
"data": { "action": "update", "id": "uuid", "payload": { } }
},
{
"idempotency_key": "req-3-uuid",
"data": { "action": "delete", "id": "uuid" }
}
]
}Response (207 Multi-Status):
{
"items": [
{
"index": 0,
"idempotency_key": "req-1-uuid",
"status": 201,
"location": "/simple-resource-registry/v1/resources/01J...",
"data": { "...full resource object..." }
},
{
"index": 1,
"idempotency_key": "req-2-uuid",
"status": 200,
"data": { "...full updated resource object..." }
},
{
"index": 2,
"idempotency_key": "req-3-uuid",
"status": 404,
"error": {
"type": "gts.cf.core.errors.err.v1~cf.core.http.not_found.v1~",
"title": "Resource not found",
"status": 404,
"detail": "Resource with id '01J...' does not exist or is not accessible",
"trace_id": "01J...-item-2"
}
}
]
}Notes:
- Follows DNA BATCH.md conventions:
POST /resources:batch, per-itemindex/status/data/error/location - Top-level HTTP status follows DNA aggregate rules: all succeeded →
200 OK; partial success/failure →207 Multi-Status; all failed with same error type → matching4xx; all failed with mixed errors →207 Multi-Status - Each item is validated and processed independently (best-effort semantics, not atomic)
- GTS type validation, access control, and behavioral flag evaluation are applied per item
- For item-level GET/PUT/DELETE semantics, inaccessible resources return 404 because tenant/user/type checks are applied as backend query filters
- Partial success is allowed — some items may succeed while others fail
idempotency_keyis required for items withaction: "create"; optional forupdate/deleteitems (used for batch-level retry safety only)- When a create item is replayed from the idempotency cache, the response item includes
"idempotency_replayed": true(per DNA BATCH.md §Idempotency) - Error responses use RFC 9457 Problem Details per item
- Event/audit emission applies per successfully processed item
- Maximum batch size: configurable (default 100)
- Maximum batch request payload: 1 MB (per DNA BATCH.md performance limits)
| Dependency Module | Interface Used | Purpose | Needed By |
|---|---|---|---|
| Types Registry | Types Registry SDK (ClientHub) | Resolve GTS type definitions, validate type, read behavioral flags | cpt-cf-srr-fr-gts-type-registration, cpt-cf-srr-fr-gts-type-validation |
| modkit-db | SecurityContext-based tenant scoping | Tenant-scoped database access infrastructure (indirect — used by database-backed storage plugins, not by the main module directly) | cpt-cf-srr-fr-default-storage-backend |
| gts-rust | Rust crate (library) | GTS ID generation, schema utilities, payload validation against GTS type schemas | cpt-cf-srr-fr-gts-type-registration, cpt-cf-srr-fr-idempotent-resource-create, cpt-cf-srr-fr-update-resource |
| Events Broker | Events Broker SDK (ClientHub) | Emit domain events for resource lifecycle (fixed schema: id, type, subject_type, subject_id) | cpt-cf-srr-fr-notification-events |
| Audit Module | Audit SDK (ClientHub) | Emit audit events for compliance (fixed schema: id, type, subject_type, subject_id, previous_payload, new_payload) | cpt-cf-srr-fr-audit-events |
Dependency Rules (per project conventions):
- No circular dependencies
- Always use SDK modules for inter-module communication
- No cross-category sideways deps except through contracts
SecurityContextmust be propagated across all in-process calls
None. Simple Resource Registry does not communicate with external systems directly. All external interactions (if any) are mediated by other Cyber Ware modules.
Event Types: resource.created, resource.updated, resource.deleted
Fixed Schema:
{
"id": "string (event id)",
"type": "string (gts.cf.core.events.event.v1~ ... created | updated | deleted)",
"subject_type": "string (GTS type ID of the resource)",
"subject_id": "uuid (resource identifier)"
}Design Rationale: The event schema is intentionally minimal and does not include the full resource payload. This keeps events lightweight and prevents event bus saturation. Consumers that need the full resource state can fetch it via GET /resources/{id} using the provided id. The fixed schema ensures consistent event handling across all resource types without requiring per-type event definitions.
Event Types: resource.created, resource.updated, resource.deleted
Fixed Schema:
{
"id": "string (event id)",
"type": "string (gts.cf.core.events.event.v1~cf.core.audit.event.v1~ created | updated | deleted)",
"subject_type": "GTS string (GTS type ID of the resource)",
"subject_id": "uuid (resource identifier)",
"previous_payload": "object | null (resource payload before operation; null for create)",
"new_payload": "object | null (resource payload after operation; null for delete)"
}Design Rationale: Audit events include full before/after payloads to enable complete audit trail reconstruction without requiring access to the live resource. For create operations, previous_payload is null. For delete operations, new_payload is null. For update operations, both fields are populated, enabling diff-based audit reporting. The fixed schema ensures consistent audit event structure across all resource types.
ID: cpt-cf-srr-seq-create
Use cases: cpt-cf-srr-usecase-store-workflow-object
Actors: any API
sequenceDiagram
participant C as Consumer
participant API as API Layer
participant SVC as ResourceService
participant ACL as GTS Access Control
participant Cache as GTS Type Cache
participant TR as Types Registry
participant SP as Storage Plugin
participant EB as Events Broker
participant AU as Audit Module
C->>API: POST /resources {type, idempotency_key, payload}
API->>API: Extract SecurityContext
API->>API: Validate request body (required fields, payload size)
API->>SVC: create_resource(ctx, type, idempotency_key, payload)
SVC->>Cache: get_type_config(type)
alt Cache miss
Cache->>TR: get_gts_entity(type)
alt GTS type not found
TR-->>Cache: not found
Cache-->>SVC: not found
SVC-->>API: 400 gts-type-not-found
API-->>C: 400 Bad Request
end
TR-->>Cache: ResourceTypeConfig
Cache->>Cache: store with TTL
end
Cache-->>SVC: ResourceTypeConfig (flags + retention)
SVC->>ACL: check_permission(ctx, type, "create")
alt Type not in caller's token GTS scope
ACL-->>SVC: denied
SVC-->>API: 403 gts-type-not-in-scope
API-->>C: 403 Forbidden
end
ACL-->>SVC: allowed
alt is_per_owner_resource=true and no subject_id in SecurityContext
SVC-->>API: 422 validation-error
API-->>C: 422 Unprocessable Entity
end
SVC->>SVC: Validate payload against GTS type schema (gts crate)
alt Payload validation fails
SVC-->>API: 422 validation-error
API-->>C: 422 Unprocessable Entity
end
SVC->>SVC: Assign UUID if id not provided, set tenant_id=ctx.subject_tenant_id,<br/>owner_id=ctx.subject_id (if per_owner), set timestamps
SVC->>SP: create(ctx, resource, idempotency_key)
Note over SP: Plugin atomically checks idempotency_key in its dedup store<br/>and persists resource + idempotency record in one transaction
alt CreateOutcome::Duplicate
SP-->>SVC: Duplicate(existing_id)
SVC-->>API: 409 duplicate-idempotency-key + existing id
API-->>C: 409 Conflict
end
SP-->>SVC: Created(resource)
alt Event flags enabled (cpt-cf-srr-fr-notification-events)
SVC->>EB: emit(resource.created)
end
alt Audit flags enabled on type or parent type (cpt-cf-srr-fr-audit-events)
SVC->>AU: emit(resource.created audit)
end
SVC-->>API: created resource
API-->>C: 201 Created
ID: cpt-cf-srr-seq-get
Use cases: cpt-cf-srr-usecase-reflect-external
Actors: any API
sequenceDiagram
participant C as Consumer
participant API as API Layer
participant SVC as ResourceService
participant ACL as GTS Access Control
participant SP as Storage Plugin
C->>API: GET /resources/{id}
API->>API: Extract SecurityContext
API->>SVC: get_resource(ctx, id)
SVC->>ACL: get_permitted_types(ctx, "read")
ACL-->>SVC: permitted GTS types from token claims
SVC->>SP: get(ctx, id)
Note over SP: Backend query applies all filters:<br/>- tenant_id = ctx.subject_tenant_id<br/>- type IN (permitted GTS types)<br/>- owner_id = ctx.subject_id (if is_per_owner_resource)<br/>- exclude soft-deleted
SP-->>SVC: resource | None
alt Resource found
SVC-->>API: resource
API-->>C: 200 OK
else Not found (filtered out or does not exist)
SVC-->>API: 404 not-found
API-->>C: 404 Not Found
end
ID: cpt-cf-srr-seq-list
Use cases: cpt-cf-srr-usecase-query-odata
Actors: any API
sequenceDiagram
participant C as Consumer
participant API as API Layer
participant SVC as ResourceService
participant ACL as GTS Access Control
participant SP as Storage Plugin
C->>API: GET /resources?$filter=...&$orderby=...&limit=20
API->>API: Extract SecurityContext
API->>API: Parse OData query parameters
API->>API: Validate: no payload field references
API->>SVC: list_resources(ctx, type_filter, odata)
SVC->>ACL: check_list_permission(ctx, type_filter, "read")
Note over ACL: If type_filter contains wildcard (*),<br/>compute intersection with caller's permitted GTS types
alt No permitted types match
ACL-->>SVC: denied
SVC-->>API: 403 gts-type-not-in-scope
API-->>C: 403 Forbidden
end
ACL-->>SVC: allowed (effective_gts_filter)
SVC->>SP: list(ctx, effective_gts_filter, odata)
Note over SP: Backend query applies all filters:<br/>- tenant_id = ctx.subject_tenant_id<br/>- type IN (effective_gts_filter)<br/>- owner_id = ctx.subject_id (if is_per_owner_resource)<br/>- exclude soft-deleted<br/>- OData $filter on schema columns<br/>- $orderby, limit, cursor
SP-->>SVC: PagedResult<Resource>
SVC-->>API: paged result
API-->>C: 200 OK {items: [...], page_info: {...}}
ID: cpt-cf-srr-seq-update
Actors: any API
sequenceDiagram
participant C as Consumer
participant API as API Layer
participant SVC as ResourceService
participant ACL as GTS Access Control
participant Cache as GTS Type Cache
participant TR as Types Registry
participant SP as Storage Plugin
participant EB as Events Broker
participant AU as Audit Module
C->>API: PUT /resources/{id} {payload}
API->>API: Extract SecurityContext
API->>SVC: update_resource(ctx, id, payload)
SVC->>ACL: get_permitted_types(ctx, "update")
ACL-->>SVC: permitted GTS types from token claims
SVC->>SP: get(ctx, id)
Note over SP: Backend query applies all filters:<br/>- tenant_id = ctx.subject_tenant_id<br/>- type IN (permitted GTS types)<br/>- owner_id = ctx.subject_id (if is_per_owner_resource)<br/>- exclude soft-deleted
SP-->>SVC: existing resource | None
alt Not found (filtered out or does not exist)
SVC-->>API: 404 not-found
API-->>C: 404 Not Found
end
SVC->>Cache: get_type_config(resource.type)
alt Cache miss
Cache->>TR: get_gts_entity(resource.type)
TR-->>Cache: ResourceTypeConfig
Cache->>Cache: store with TTL
end
Cache-->>SVC: ResourceTypeConfig (flags + retention)
SVC->>SVC: Validate payload against GTS type schema (gts crate)
alt Payload validation fails
SVC-->>API: 422 validation-error
API-->>C: 422 Unprocessable Entity
end
SVC->>SVC: Update payload, set updated_at
SVC->>SP: update(ctx, resource)
SP-->>SVC: updated resource
alt Update event flag enabled (cpt-cf-srr-fr-notification-events)
SVC->>EB: emit(resource.updated)
end
alt Update audit flag enabled on type or parent type (cpt-cf-srr-fr-audit-events)
SVC->>AU: emit(resource.updated audit)
end
SVC-->>API: updated resource
API-->>C: 200 OK
ID: cpt-cf-srr-seq-delete
Actors: any API
sequenceDiagram
participant C as Consumer
participant API as API Layer
participant SVC as ResourceService
participant ACL as GTS Access Control
participant Cache as GTS Type Cache
participant TR as Types Registry
participant SP as Storage Plugin
participant EB as Events Broker
participant AU as Audit Module
C->>API: DELETE /resources/{id}
API->>API: Extract SecurityContext
API->>SVC: delete_resource(ctx, id)
SVC->>ACL: get_permitted_types(ctx, "delete")
ACL-->>SVC: permitted GTS types from token claims
SVC->>SP: get(ctx, id)
Note over SP: Backend query applies all filters:<br/>- tenant_id = ctx.subject_tenant_id<br/>- type IN (permitted GTS types)<br/>- owner_id = ctx.subject_id (if is_per_owner_resource)<br/>- exclude soft-deleted
SP-->>SVC: existing resource | None
alt Not found (filtered out or does not exist)
SVC-->>API: 404 not-found
API-->>C: 404 Not Found
end
SVC->>Cache: get_type_config(resource.type)
alt Cache miss
Cache->>TR: get_gts_entity(resource.type)
TR-->>Cache: ResourceTypeConfig
Cache->>Cache: store with TTL
end
Cache-->>SVC: ResourceTypeConfig (flags + retention)
alt retention_days == 0 (immediate hard-delete)
SVC->>SP: hard_delete(ctx, id)
Note over SVC,SP: Permanently deletes the single resource row — no soft-delete step
SP-->>SVC: ok
else Normal soft-delete
SVC->>SP: delete(ctx, id)
SP->>SP: Set deleted_at = now()
SP-->>SVC: ok
end
alt Delete event flag enabled (cpt-cf-srr-fr-notification-events)
SVC->>EB: emit(resource.deleted)
end
alt Delete audit flag enabled on type or parent type (cpt-cf-srr-fr-audit-events)
SVC->>AU: emit(resource.deleted audit)
end
SVC-->>API: ok
API-->>C: 204 No Content
ID: cpt-cf-srr-seq-storage-routing
Actors: any API
sequenceDiagram
participant SVC as ResourceService
participant Router as Storage Router
participant Config as Routing Config
participant RelDB as Relational DB Plugin
participant SP as Search Plugin
SVC->>Router: resolve_plugin(type)
Router->>Config: lookup(type)
alt Mapped to search plugin
Config-->>Router: search-engine
Router-->>SVC: Search Plugin
SVC->>SP: operation(ctx, ...)
else Default (Relational DB)
Config-->>Router: relational-db (default)
Router-->>SVC: Relational DB Plugin
SVC->>RelDB: operation(ctx, ...)
end
ID: cpt-cf-srr-seq-retention-purge
Actors: System (Jobs Manager job)
sequenceDiagram
participant JM as Jobs Manager
participant Task as Retention Purge Jobs Manager job
participant TR as Types Registry
participant SP as Storage Plugin
loop Periodic (e.g., every 1 hour)
JM->>Task: run(job)
Task->>TR: list_gts_entities(base_resource_type pattern)
TR-->>Task: ResourceTypeConfig[] (with retention settings)
loop For each resource type
Task->>Task: Compute cutoff = now() - retention_days
Note over Task: retention_days from type config,<br/>or system default (30 days) if null
Task->>SP: purge_deleted_before(type, cutoff, batch_size)
SP->>SP: Hard-delete matching resources<br/>where type matches and deleted_at < cutoff
SP-->>Task: purged_count
end
end
The following relational schema is the recommended starting point for all relational storage backends. It maps directly to the logical Resource entity (§3.1) and is used by the default plugin (srr-rdb-plugin). Non-relational backends may use any internal structure but MUST persist the same logical fields and support equivalent query patterns.
Table: simple_resources
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
UUID | NOT NULL | Primary key (system-generated or caller-supplied) |
type |
VARCHAR(512) | NOT NULL | GTS type identifier |
tenant_id |
UUID | NOT NULL | Tenant owner (from SecurityContext.subject_tenant_id) |
owner_id |
UUID | NULL | Subject owner (set when is_per_owner_resource=true) |
created_at |
TIMESTAMP WITH TIME ZONE | NOT NULL | Creation timestamp |
updated_at |
TIMESTAMP WITH TIME ZONE | NOT NULL | Last update timestamp |
deleted_at |
TIMESTAMP WITH TIME ZONE | NULL | Soft-delete timestamp (NULL = active) |
payload |
TEXT | NOT NULL | Serialized JSON payload (max 64 KB, validated at API layer) |
Primary Key: id
Indexes (baseline set aligned with the primary query patterns in §3.3):
| Index | Columns | Purpose |
|---|---|---|
idx_tenant_type |
(tenant_id, type) |
List resources of a type within a tenant |
idx_tenant_type_created |
(tenant_id, type, created_at) |
OData $orderby created_at within a type |
idx_tenant_owner |
(tenant_id, owner_id) |
User-scoped resource queries |
idx_type_deleted |
(type, deleted_at) |
Retention purge job |
These indexes cover the leading filter predicates for the primary query patterns. They do not cover all predicate combinations, ordering requirements, or selectivity scenarios. Production deployments may require additional indexes depending on actual query distribution and data volume.
Benefits: predictable query performance across plugins; consistent migration paths; easier plugin implementation — new backends start from a known-good schema rather than inventing their own.
Payload size limit: Resource payloads are limited to 64 KB (see cpt-cf-srr-constraint-payload-size). Validated at the API layer before reaching the plugin.
For the default relational database backend's complete schema (including the idempotency keys table, group memberships table, and full indexing strategy), see:
- Plugin DESIGN:
plugins/srr-rdb-plugin/docs/DESIGN.md - Storage optimization ADR:
plugins/srr-rdb-plugin/docs/ADR/0001-cpt-cf-srr-rdb-adr-storage-optimization.md
Other storage backends MUST implement equivalent persistence for the Resource entity and idempotency deduplication (see CreateOutcome in §3.2), but may use any internal data structure appropriate to their storage technology.
-
No payload querying: The JSON payload is stored as an opaque blob. Consumers cannot filter, sort, or search within payload fields via the API. This is a deliberate trade-off for simplicity and predictable performance. For search-heavy use cases, the search API (
cpt-cf-srr-fr-search-api) with a search-capable plugin is the intended solution. -
Default backend scalability: The default relational DB backend targets up to 100M resources (
cpt-cf-srr-nfr-scalability). Beyond this threshold, per-resource-type storage routing (cpt-cf-srr-fr-storage-routing) enables dedicated backends for high-volume types. Storage optimization strategy is a per-backend concern — see the plugin DESIGN and its ADRs. -
No cross-type queries (except via wildcard): The API does not support querying resources across arbitrary, unrelated GTS types in a single request. GTS wildcard filtering (trailing
*) enables querying within a type hierarchy, but consumers needing views across unrelated types must make multiple requests or use an aggregation layer. -
Payload validation at API boundary: The Simple Resource Registry service validates resource payloads against the derived GTS type's JSON Schema at create and update time, using the
gtscrate's standard schema validation. Validation failures return 422 Unprocessable Entity. This ensures that all persisted resources conform to their declared GTS type schema, regardless of whether the request originates from the REST API or the SDK client. -
No resource versioning: The module does not maintain a history of resource changes. Only the current state is stored. Consumers needing change history should use audit events (
cpt-cf-srr-fr-audit-events) for traceability. -
Retention purge is background-only: The retention purge task runs periodically, so soft-deleted resources may persist slightly beyond their configured retention period depending on task frequency and batch size. Operators should size the purge interval and batch size appropriately for their workload.
-
No auto-deletion of active resources: The module intentionally does not support automatic soft-deletion of active resources based on a TTL. This is an intentional safety decision — automatic deletion of live data is error-prone and must not be accidentally enabled. Consumers requiring resource TTL must implement their own cleanup logic or use workflows.
-
GTS access control depends on Permission quality: If the caller's token lacks properly scoped GTS
resource_patternentries, all operations will be denied. This is by design — permissive tokens (e.g.,resource_pattern: "*") are the responsibility of the identity provider and policy engine.
| Use Case | Description |
|---|---|
| Workflow artifacts | Custom objects generated by workflow automation steps |
| External object reflections | Entries representing objects from external systems (CRM contacts, ticketing issues) |
| Domain model consistency | Partial representations of external resources for internal domain model coherence |
| Agent execution artifacts | Intermediate or final results from agent pipeline execution |
| Tenant configuration objects | Custom tenant-level settings not covered by dedicated modules |
| User preferences | Per-user settings or state for platform features |
simple_resource_registry:
storage:
default_plugin: relational-db
routes:
- type: "gts.cf.core.srr.resource.v1~vendor.pkg.*"
plugin: search-engine
- type: "gts.cf.core.srr.resource.v1~cf.core.*"
plugin: relational-db
# Vendor-provided plugin bridging to an existing platform CMDB
- type: "gts.cf.core.srr.resource.v1~vendor1.*"
plugin: vendor1-cmdb-adapterThe routing configuration maps resource type patterns (GTS type IDs / GTS type IDs) to storage plugin instances. Glob patterns enable grouping related types to the same backend. Unmatched types fall back to the default plugin. Platform vendors can register their own plugin implementations (e.g., vendor-cmdb-adapter) to bridge resource operations to existing platform storage components, enabling multiple coexisting plugins for different resource families.
This section records findings from cross-referencing this design against docs/arch/authorization/DESIGN.md and the gts-rust library.
Per docs/arch/authorization/DESIGN.md, the canonical SecurityContext struct fields are:
SecurityContext {
subject_id: String, // unique subject identifier (maps to resource.owner_id)
subject_type: Option<GtsTypeId>, // optional vendor subject type
subject_tenant_id: TenantId, // Subject Owner Tenant (maps to resource.tenant_id)
token_scopes: Vec<String>, // capability ceiling (["*"] for first-party)
bearer_token: Option<Secret<String>>, // original token for PDP forwarding
}Throughout this document, ctx.subject_tenant_id is used for resource.tenant_id (mandatory) and ctx.subject_id for resource.owner_id (when is_per_owner_resource=true).
docs/arch/authorization/DESIGN.md defines Permission as { resource_type, action }. This design uses resource_pattern in place of resource_type to emphasize GTS wildcard-based pattern matching semantics (see GtsWildcard in gts-rust). These are the same concept; resource_pattern is the SRR-specific terminology for a resource_type value that may contain a GTS trailing wildcard (*).
- ID validation: uses
GtsID::new(id)fromgts-rust/gts/src/gts.rs; GTS type IDs end with~(e.g.,gts.cf.core.srr.resource.v1~) - Wildcard matching: uses
GtsWildcard::new(pattern)+gts_id.wildcard_match(&pattern)fromgts-rust/gts/src/gts.rs - Schema validation: uses
gtscrate instance validation (OP#6) pergts-rustREADME - In-memory cache: follows
Arc<RwLock<HashMap<String, V>>>pattern fromgts-rust/gts/src/store.rs; aligns with the "In-memory entities registry" feature ofgts-rust - GTS URI prefix: JSON Schema
$idfields usegts://prefix (GTS_URI_PREFIXingts-rust); confirmed in base schema definition