Overlays: sharing a schema across companies
Let many companies extend one shared schema with private fields.
Overlays are OpenSchema's answer to a specific, hard problem:
Several companies want to use the same core schema, but each needs to add a few private fields of its own — and they must be able to do this without coordinating with each other or with the team that owns the shared schema.
This guide explains the problem, the mechanism, and how it generates code.
The problem with the obvious approaches
Suppose Person is a shared model:
namespace myorg
model Person {
1 name: string
2 email: string?
}
ACME wants to add a loyaltyTier; Globex wants to add a riskScore. The naive
options all fail:
- Edit the shared file. Now the shared schema is polluted with every company's private fields, and ACME can see Globex's.
- Fork the schema. The whole point was to share it; forks drift.
- Just append fields with new ordinals. ACME adds field
3, Globex also adds field3— collision. Or ACME uses3and Globex uses4, but now they must coordinate ordinals forever, and adding a field to the shared schema (say a new field3) silently clashes with ACME's private3.
The root issue is that all these approaches share one ordinal space.
The overlay mechanism
An overlay is a company-scoped set of private fields layered onto a base model. Crucially, it has its own independent ordinal space that starts at 1:
// acme-overlay.schema
namespace myorg.acme
import { Person } from "./people"
overlay acme on Person {
1 loyaltyTier: string
2 accountManagerId: u32
}
// globex-overlay.schema
namespace myorg.globex
import { Person } from "./people"
overlay globex on Person {
1 riskScore: f32
}
Field identity in an overlay is the pair (company, ordinal). ACME's 1 and
Globex's 1 are different fields because the company differs. Therefore:
- Two companies can both start at ordinal
1and never collide. - The shared-schema team can add fields
3,4, ... toPersonforever without ever threatening any company's overlay, because overlay ordinals live in a separate space. - A company controls its own overlay's ordinals, so the only collisions possible are within one company's own overlay — which the compiler checks.
Rules the compiler enforces
When resolving overlays, the compiler reports:
| Code | Meaning |
|---|---|
OS3001 |
The overlay's base model does not exist. |
OS3002 |
The named base is not a model (e.g. it's an enum). |
OS3003 |
An overlay field name collides with a field already on the base model. |
OS2002 |
Two fields in the same overlay reuse an ordinal. |
OS3004 |
Two overlays define the same company on the same base model. |
Note OS3003: overlays add new names. They cannot shadow or redefine a base
field — an overlay is strictly additive.
Inline private vs. overlays
There are two ways to keep fields out of public artifacts, for two different owners:
privatefields belong to the schema owner. They live in the base model, in its ordinal space. Use them for the owning team's internal fields.model Person { 1 name: string 100 private fraudScore: f32? // the owning team's internal field }Overlays belong to a different company consuming the shared schema. They live in their own ordinal space and are selected by
--company.
Both can coexist on the same model.
Generating a company's view
Generate the base schema plus one company's overlay with --company:
openschema gen acme-overlay.schema --target sql --out ./out --company acme
The overlay fields are added to the base model's table, namespaced by company so they cannot collide with base columns or another company's columns:
CREATE TABLE orders (
id UUID NOT NULL PRIMARY KEY DEFAULT gen_uuid(),
customer_id UUID NOT NULL REFERENCES customer(id),
status TEXT NOT NULL,
-- ... base columns ...
acme_loyalty_tier TEXT NOT NULL,
acme_account_manager_id BIGINT NOT NULL
);
Without --company, you get just the shared schema — the public view that every
company shares. With --company acme you get ACME's view; with
--company globex, Globex's. The two never see each other's fields.
Overlays work across every target. In TypeScript the overlay fields join the interface; in GraphQL they join the type; in all cases only the selected company's fields appear.
A complete example
The repository's examples/ecommerce/ directory is a working multi-file,
multi-company project:
common.schema— shared value types (Money,PostalAddress,Region).orders.schema— theOrderaggregate and its API, importingcommon.acme-overlay.schema— ACME's private overlay, importingorders.
Generate the shared view and ACME's view and compare:
openschema gen examples/ecommerce/orders.schema --target sql --out ./shared
openschema gen examples/ecommerce/acme-overlay.schema --target sql --out ./acme --company acme
The overlay file imports Order (which itself imports common), so the whole
graph resolves, and ACME's acme_* columns appear on the shared orders table.