Skip to main content


The Ent represents a single node in the graph. Generated from the schema, it can be configured as needed based on the application.

Each generated Ent implements the following interface:

interface Ent<TViewer extends Viewer = Viewer> {
id: ID;
viewer: TViewer;
getPrivacyPolicy(): PrivacyPolicy<this, TViewer>;
nodeType: string;



id of the Ent. usually the primary key in the database.


The Viewer who loaded the Ent. The Privacy Policy associated with this Ent must permit the viewer to see this Ent otherwise it's not returned by the system.


Privacy Policy used to determine if the Viewer can see this object.

This policy is applied everywhere the Ent is loaded. This is good because we don't need permissions checks all over the place. We can trust that anytime we encounter one of these objects, we know the right checks have been done and we can call things safely with no worries.


Generated by the system, unique per object type, used to differentiate between different nodes. Stored in id1_type or id2_type columns in the database when an edge is written to or from this node.

Also used when this is id is stored in a polymorphic column.


Given the following schema:

import { EntSchema, StringType } from "@snowtop/ent"; 
import { EmailType } from "@snowtop/ent-email";
import { PasswordType } from "@snowtop/ent-password";

const UserSchema = new EntSchema({
fields: {
FirstName: StringType(),
LastName: StringType(),
EmailAddress: EmailType(),
Password: PasswordType(),

we have the following classes generated

//... missing imports
export class UserBase {
readonly nodeType = NodeType.User;
readonly id: ID;
readonly createdAt: Date;
readonly updatedAt: Date;
readonly firstName: string;
readonly lastName: string;
readonly emailAddress: string;
protected readonly password: string;

constructor(public viewer: Viewer, data: Data) { =;
this.createdAt = data.created_at;
this.updatedAt = data.updated_at;
this.firstName = data.first_name;
this.lastName = data.last_name;
this.emailAddress = data.email_address;
this.password = data.password;

// default privacyPolicy is Viewer can see themselves
getPrivacyPolicy(): PrivacyPolicy<this> {
return AllowIfViewerPrivacyPolicy;

static async load<T extends UserBase>(
this: new (viewer: Viewer, data: Data) => T,
viewer: Viewer,
id: ID,
): Promise<T | null> {
return loadEnt(viewer, id, UserBase.loaderOptions.apply(this));

static async loadX<T extends UserBase>(
this: new (viewer: Viewer, data: Data) => T,
viewer: Viewer,
id: ID,
): Promise<T> {
return loadEntX(viewer, id, UserBase.loaderOptions.apply(this));
/// more...
import { UserBase } from "src/ent/internal"; 

export class User extends UserBase {}

The UserBase class is where all generated code related to User is put and is regenerated everytime the schema is changed. The User class is generated once and then the developer can add new functionality there over time as needed.

Privacy Policy

The default PrivacyPolicy that comes with the framework is that the Viewer can see themselves. This usually isn't sufficient for any real application so we provide a way to override that. This can be done by just overriding the getPrivacyPolicy method in the User class.

For example, to make it so that anyone can see any User, you can change src/ent/user.ts as follows:

import { UserBase } from "src/ent/internal";
import { AlwaysAllowPrivacyPolicy, PrivacyPolicy } from "@snowtop/ent"

export class User extends UserBase {
getPrivacyPolicy() {
return AlwaysAllowPrivacyPolicy;

This new Policy takes precedence over the default one and now the User is visible to anyone.


To load a User , a Viewer and its id are needed

const user = await User.load(viewer, id);

That performs the privacy check and either returns the ent associated with the id or returns null.

If you know the ent is loadable or want to throw an exception if it's not, you can use the loadX variant of the same API:

const user = await User.loadX(viewer, id);

Custom functionality

To add custom functionality, just add it in the User class.

For example, to return how long the user's account has existed:

import { UserBase } from "src/ent/internal"; 
import { AlwaysAllowPrivacyPolicy, ID, LoggedOutViewer, PrivacyPolicy } from "@snowtop/ent"
import { Interval } from "luxon";

export class User extends UserBase {
getPrivacyPolicy() {
return AlwaysAllowPrivacyPolicy;

howLong() {
return Interval.fromDateTimes(this.createdAt, new Date()).count('seconds');

and when the User is loaded, can access the new method since an instance of User is what's returned.

  const user = await User.loadX(new LoggedOutViewer(), id);

Nested objects

Because the privacy policy is applied everywhere an object is loaded consistently, the framework can provide helpful accessors by default.

For example, given the following schema:

const EventSchema = new EntSchema({
fields: {
/// ... more fields
creatorID: UUIDType({
foreignKey: { schema: "User", column: "id" },
// or
creatorID: UUIDType({
fieldEdge: { schema: "User", inverseEdge: "createdEvents" },
export default EventSchema;

the following accessors are added:

export class EventBase {
/// bunch of stuff

loadCreator(): Promise<User | null> {
return loadEnt(this.viewer, this.creatorID, User.loaderOptions());

loadCreatorX(): Promise<User> {
return loadEntX(this.viewer, this.creatorID, User.loaderOptions());

and when an event is loaded, we can easily load the creator afterwards and be confident that the returned creator is visible since we pass the viewer down and check the permissions before returning.

const event = await Event.loadX(viewer, id);
const creator = await event.loadCreatorX();