Software engineers have learned how to engineer APIs quite well.
For an API, we routinely think about:
- contracts
- schemas
- versioning
- backward compatibility
- breaking changes
- authentication
- authorization
- ownership
- documentation
- discovery
- lifecycle and deprecation
If I expose:
POST /customers
I can define exactly:
- what the request looks like
- what the response looks like
- who can call it
- which version is being used
- whether a change breaks existing clients
We have OpenAPI, OAuth, API gateways, schema validation, versioning rules, compatibility tools, and mature practices around all of this.
But modern software is no longer made only of APIs.
We also exchange and operate on:
- events
- configuration objects
- user and tenant settings
- workflows
- serverless function contracts
- MCP tools
- AI agents
- prompts
- policies
- extension manifests
- plugin-defined data
These artifacts are contracts too.
But they are often still handled by separate, ad-hoc mechanisms.
Imagine you are designing an event platform
Assume the platform is not just a REST API.
Events may arrive through:
- gRPC
- Kafka
- WebSockets
- internal queues/SDKs
So we cannot simply say:
OpenAPI validates everything for us.
The event system itself needs to understand the contracts.
Suppose the platform has events like:
Event
├── Application X Activated
│
├── MCP Agent Y
│ ├── Work Started
│ └── Work Completed
│
└── Audit Event
├── User Authentication Failure
└── User Logged In
The platform itself defines:
Event
Audit Event
User Authentication Failure
User Logged In
while external applications, agents and vendors may introduce their own event types, e.g.:
MCP Agent Y -> Work Started
Let's consider a common Event
Every event should contain common fields:
{
"timestamp": "...",
"tenantId": "...",
"eventType": "...",
"payload": "..."
}
Call this schema:
Event
Now audit events need some additional common fields:
...
"payload": {
"userId": "...",
"ipAddress": "...",
"auditData": "..."
}
So we define:
Audit Event
as a more specialized form of:
Event
Then specific audit events add their own fields.
For example:
User Authentication Failure
could additionally require:
...
"auditData": {
"error": "Invalid password"
}
while:
User Logged In
does not need an error.
So the relationship is:
Event
timestamp
tenantId
eventType
↓
Audit Event
userId
ipAddress
↓
User Authentication Failure
error
and:
Event
↓
Audit Event
↓
User Logged In
The important idea is simple:
A more specific event inherits (or specifies) the contract of the more general event above it.
So a User Authentication Failure must satisfy:
Event fields
+
Audit Event fields
+
Authentication Failure fields
Likewise, User Logged In must satisfy:
Event fields
+
Audit Event fields
+
User Logged In fields
Here we can call this type derivation, by analogy with traditional programming languages.
It is similar to inheritance in programming languages, but applied to schemas and data exchanged between independent systems.
How do we express this relationship?
Programming languages solve this naturally.
You might write:
class Event:
timestamp: datetime
tenant_id: str
class AuditEvent(Event):
user_id: str
ip_address: str
class AuthenticationFailure(AuditEvent):
error: str
But now these types are not Python classes.
They are JSON objects traveling between different systems.
JSON Schema can describe each structure.
But we also want the platform to know:
Authentication Failure derives from Audit Event
Audit Event derives from Event
That relationship becomes useful for much more than validation.
How should the event system validate an incoming event?
Suppose this arrives:
{
"timestamp": "2026-10-03T10:00:00Z",
"tenantId": "customer-12",
"eventType": "user_authentication_failure",
"userId": "42",
"payload": {
"ipAddress": "10.1.2.3",
"auditData": {
"error": "Invalid password"
}
}
}
The event system should be able to answer:
- What type is this event?
- Where is its schema?
- Is that schema registered?
- Does this object satisfy the schema?
Who can produce an event?
Now security appears.
Suppose:
Application X
is allowed to produce:
Application X Activated
Should it also be allowed to emit:
User Logged In
Probably not.
Otherwise any application could impersonate the authentication subsystem simply by sending JSON with the correct fields.
We may want rules like:
Application X
may produce
Application X events
Authentication Service
may produce
Audit authentication events
This is similar to API authorization.
With APIs where every resource type is explicit, we might say:
service A can call /billing/*
For events, we want to say:
service A can produce this family of event types
Who can read an event?
The same applies to consumers.
Perhaps a normal application can read:
- Application X Activated
- Job Started
- Job Completed
but it should not see audit events.
An administrator might be allowed to read:
all Event types
while an auditor might receive:
Audit Event
and everything derived from Audit Event
This becomes especially useful as the system grows.
If tomorrow the platform adds:
- Password Changed
- API Token Created
- Administrative Permission Changed
we don't want to rewrite every security policy.
They are all still:
Audit Event
So a rule defined for the parent type can apply automatically to the new derived types.
What happens when the common Event contract changes?
Suppose millions of events have already been stored.
The original schema was:
Event v1
- timestamp
- tenantId
- eventType
Later we decide that every event should also have origin as v1.1:
- origin
Now the questions become much more practical:
- What happens to all historical events that don't contain origin?
- Are they still valid?
- Should the new field be optional?
- Can it later become required?
- When an API returns an old event, which schema should it claim to follow?
- Should the API transform old events and populate origin?
- Can new consumers safely read both versions?
- How long must the platform support v1 for ingest?
- Which producers still send v1?
- Can we automatically determine whether v1.1 is compatible with v1?
This is not very different from evolving an API.
But the artifact is an event type, not an API endpoint resource.
The same compatibility problem still exists.
What if Audit Event changes?
Now imagine adding:
- sessionId
to Audit Event.
That change should affect:
- User Authentication Failure
- User Logged In
- Password Changed
but it should not affect:
- Application X Activated
- MCP Agent Work Started
So the platform needs to understand the hierarchy:
Event
├── Application X Activated
├── MCP Agent Work Started
└── Audit Event
├── User Authentication Failure
└── User Logged In
That allows it to answer:
- Which schemas may be affected by this change?
- Which stored data uses previous versions?
- Which producers need to be upgraded?
- Which consumers may break?
External vendors make this harder
Now imagine that the platform supports third-party applications.
The platform defines its core event hierarchy:
Event
└── Audit Event
├── User Authentication Failure
└── User Logged In
But Vendor B installs an integration and introduces:
Event
└── Integration B Connection Failed
Vendor C installs an AI product and introduces:
Event
└── AI Agent C Started
Both should still be valid platform events.
Conceptually:
Event
├── Application X Activated
├── Integration B Connection Failed
├── AI Agent C Started
└── Audit Event
├── User Authentication Failure
└── User Logged In
Now several new questions appear.
Who owns:
Integration B Connection Failed
How do we prevent another vendor from registering a type with the same name?
- Which application is allowed to emit it?
- Can Vendor B extend one of the platform's common types?
- What happens when Vendor B publishes version 2?
- Which consumers accept version 1, version 2, or both?
This is where simple local names stop being sufficient.
Events are not special
Events are just an easy example.
The same problem appears with many other data types.
Consider platform-managed data such as:
- User Settings
- Subscription Settings
- VM Settings
- Application Settings
- Integration Settings
- ...
The platform may want to provide:
storage
validation
versioning
access control
discovery
while allowing installed applications and integrations to add their own attributes or specialized types.
One integration may define additional subscription settings.
Another vendor may introduce additional VM properties.
A plugin may add user-level settings.
You immediately get the same questions:
- Who owns this data type?
- What does it derive from?
- Which version is stored?
- Who can read it?
- Who can modify it?
- Can another vendor reference it?
- Can an extension add fields without breaking existing consumers?
- What happens to old stored objects after the schema evolves?
MCP tools have the same problem
Suppose an MCP tool declares:
Input:
Repository
Output:
Code Analysis
What exactly is:
Repository
- Is it a local name?
- A GitHub repository?
- A generic source-code repository type?
- Which vendor defines it?
- Which version?
Can the tool accept a specialized subtype such as:
GitHub Repository
Can it accept every type derived from:
Repository
Then security questions appear:
- Who is allowed to invoke this tool?
- Which types of data may be passed into it?
- May a third-party MCP tool receive sensitive configuration?
- Which output types is it allowed to create?
- Can its output be passed safely to another agent?
Again, these look surprisingly similar to the problems we already solved around APIs.
The missing common layer
APIs have:
identity
contracts
versions
compatibility
ownership
security
references
But for many other artifacts we repeatedly create separate mechanisms:
event registry
schema registry
config registry
agent registry
MCP registry
function registry
workflow registry
Each eventually needs some version of:
name
owner
schema
version
references
permissions
compatibility
Perhaps these are not completely separate problems, and they need a shared type system underneath them.
This is the idea behind Global Type System
This is what we are exploring with Global Type System — GTS.
GTS is an open specification and open-source project for identifying and referencing data types and their instances.
The specification is language-independent, with JSON and JSON Schema as its primary current focus.
A GTS type identifier looks like:
gts.acme.billing.events.invoice_created.v1~
Its structure is:
gts.<vendor>.<package>.<namespace>.<type>.v<major>[.<minor>]~
For example:
vendor = acme
package = billing
namespace = events
type = invoice_created
version = v1
The trailing ~ identifies a type.
GTS can also identify instances of types.
The project is developed openly, and the specification repository includes its license and implementation rules.
Why put all of this into the identifier?
Because one identifier can carry several useful pieces of information.
1. Type or instance identity
A GTS identifier can identify:
a schema/type
or:
a concrete instance of that type
So the same identification model applies to both definitions and data.
2. Ownership
The identifier encodes:
vendor
package
namespace
type
For example:
gts.vendor_b.integration.events.connection_failed.v1~
and:
gts.vendor_c.ai.events.agent_started.v1~
can coexist without colliding.
The ownership is visible directly from the identifier.
3. Versioning
Version is part of the type identity:
gts.acme.billing.events.invoice_created.v1~
gts.acme.billing.events.invoice_created.v1.1~
gts.acme.billing.events.invoice_created.v2~
This gives tooling something deterministic to reason about when schemas evolve.
4. Type inheritance and derivation
GTS identifiers can be chained to represent that one type derives from another.
Conceptually:
Event
↓
Audit Event
↓
User Authentication Failure
The derived type keeps the relationship to its parent types.
That means tooling can determine not only:
this is User Authentication Failure
but also:
this is an Audit Event
this is also an Event
This relationship can then be used for validation, discovery, subscriptions, or policy decisions.
5. Access control
Because identifiers have predictable namespaces, security policies can work with exact identifiers or wildcard patterns.
For example:
gts.acme.billing.events.*
could represent all billing event types belonging to the package.
A policy system could therefore express rules such as:
Service A may produce:
gts.acme.billing.events.*
User B may read:
gts.acme.public.events.*
Auditor may read:
Audit Event and its derived types
The policy engine itself is separate from GTS.
GTS provides the structured identity on which the policy can operate.
6. References between data types
Types also need to refer to other types.
This is effectively the schema equivalent of a foreign key.
For example:
Invoice
references
Customer
or:
Agent
references
Tool
Instead of referencing a vendor-specific local schema name, the reference can point to a globally identified type.
That becomes important when different organizations publish schemas independently.
The larger idea
Programming languages gave us type systems inside applications.
API engineering gave us strong contracts, versions, compatibility rules, and security between services.
But modern platforms now exchange much more than API requests.
They exchange:
events
settings
configs
workflows
tool contracts
agent data
function definitions
policies
extension data
Those artifacts increasingly cross application, vendor, and organizational boundaries.
They need many of the same properties APIs already have.
GTS is an attempt to provide a common type identity and relationship layer for that broader software ecosystem.
It is an open specification and open-source project:
https://github.com/GlobalTypeSystem/gts-spec
https://github.com/GlobalTypeSystem
I'm particularly interested in how people building event platforms, agent ecosystems, MCP infrastructure, schema registries, plugin systems, and extensible SaaS products deal with these questions today.
Are we dealing with separate problems?
Or are we repeatedly rebuilding fragments of the same missing type system?
Top comments (0)