Global Schema
global_schema lets you describe reusable fields, structs, enums, and even edge behavior once and then opt into them from any schema. Use it when multiple Ents share the exact same JSON payload, enum values, or edge lifecycle rules so you only need to maintain a single definition.
File location and setup
Codegen looks for a file that exports a GlobalSchema object. The default location is src/schema/__global__schema.ts, but you can override it via globalSchemaPath in ent.yml:
globalSchemaPath: global_schema.ts
The generated src/ent/internal.ts automatically wires the file up by calling setGlobalSchema, so every entrypoint that imports anything from src/ent gets the global schema for free (see examples/todo-sqlite/src/ent/internal.ts). If you have a custom entrypoint that bypasses src/ent, make sure you call setGlobalSchema yourself before touching any code that relies on the shared types.
Declaring shared fields
Define globals inside the fields map. Each entry is a regular field factory, so you can compose nested structs, imported custom types, or globals from other packages. Below is an abbreviated version of examples/todo-sqlite/src/schema/global_schema.ts:
import { BooleanType, GlobalSchema, StringType, StructType, StructTypeAsList } from "@snowtop/ent/schema/";
import { GlobalDeletedEdge } from "@snowtop/ent-soft-delete";
const globalSchema: GlobalSchema = {
fields: {
account_prefs: StructType({
tsType: "AccountPrefs",
graphQLType: "AccountPrefs",
fields: {
finishedNux: BooleanType(),
enableNotifs: BooleanType(),
preferredLanguage: StringType(),
},
}),
countries: StructTypeAsList({
tsType: "Country",
graphQLType: "Country",
fields: {
name: StringType(),
code: StringType(),
},
}),
},
...GlobalDeletedEdge,
};
export default globalSchema;
Referencing a global struct
Once a struct exists globally, reference it from any schema by passing globalType to StructType or StructTypeAsList. The field still accepts local options (nullable, privacy, defaults, etc.), but the shape, GraphQL type name, and generated TypeScript type come from the global definition.
const AccountSchema = new TodoBaseEntSchema({
fields: {
accountPrefs: StructType({ nullable: true, globalType: "AccountPrefs" }),
accountPrefs3: StructType({
globalType: "AccountPrefs",
serverDefault: { finishedNux: false, enableNotifs: false, preferredLanguage: "en_US" },
}),
accountPrefsList: StructTypeAsList({ nullable: true, globalType: "AccountPrefs" }),
country_infos: StructTypeAsList({
tsType: "CountryInfo",
fields: { countries: StructTypeAsList({ globalType: "Country" }) },
}),
},
});
Every place that references AccountPrefs now reuses a single type AccountPrefs, input AccountPrefsInput, and JSON serialization logic. GraphQL clients only learn a single object shape regardless of which Ent returns it.
Reusing enum definitions
Shared enums also live inside fields. You get to specify the canonical list of values once and refer to it via globalType anywhere you need the enum. This keeps the generated TypeScript union and GraphQL enum synchronized.
const globalSchema: GlobalSchema = {
fields: {
tag: EnumType({
tsType: "GuestTag",
graphQLType: "GuestTag",
values: ["friend", "coworker", "family"],
disableUnknownType: true,
}),
},
};
const GuestSchema = new EntSchema({
fields: {
tag: EnumType({ globalType: "GuestTag", nullable: true }),
},
});
The same approach works for EnumListType, IntegerEnumType, and IntegerEnumListType because they all delegate back to the global enum definition when globalType is set.
Global edges and edge transforms
Besides fields, the global schema can change how association edges behave everywhere:
edges: declare association edges that do not naturally belong to a single schema. Codegen creates tables and edge constants for them just like schema-scoped edges. (Seeexamples/simple/src/schema/__global__schema.tsfor aloginAuthedge that points toUser.)extraEdgeFields: add columns to every edge table (including edge groups and global edges). They are processed like fields, so you can set defaults, validation, etc.transformEdgeRead: return a clause that automatically augments every edge query. This is perfect for things like soft-delete filters.transformEdgeWrite: intercept insert/update/delete operations on edges so you can rewrite them.
The soft-delete helper from @snowtop/ent-soft-delete is a practical example. It adds a deleted_at column to all edge tables, automatically filters it out during reads, and converts deletes into updates:
export const GlobalDeletedEdge = {
extraEdgeFields: {
deleted_at: TimestampType({ nullable: true, defaultValueOnCreate: () => null }),
},
transformEdgeRead(): Clause {
return query.Eq("deleted_at", null);
},
transformEdgeWrite(stmt) {
if (stmt.op === SQLStatementOperation.Delete) {
return { op: SQLStatementOperation.Update, data: { deleted_at: new Date() } };
}
return null;
},
};
Spreading that object inside your global schema (as the todo-sqlite example does) means every edge created through actions, loaders, or queries respects the soft-delete contract without touching individual schemas.
Putting it all together
- Create a
GlobalSchemafile (either atsrc/schema/__global__schema.tsor whatever you set asglobalSchemaPath). - Describe shared structs/enums in the
fieldsmap and optionally set up edge-wide behavior viaedges,extraEdgeFields,transformEdgeRead, ortransformEdgeWrite. - Reference the shared types with
globalTypewherever you need them. You can still tweak per-field options to fit each schema. - Run the CLI (
tsent codegenor whichever wrapper you use in your project) so the new global definitions show up in the generated Ent, GraphQL schema, and SQL migrations.
With this setup you only define complex payloads, enums, and edge behavior once, which keeps your schemas consistent and eliminates the drift that happens when every team copies the same JSON field by hand.