Cerial
Inheritance

Abstract Models

Template models that exist only for inheritance — no table, no types, no client access.

Abstract models define reusable field sets that other models can extend. They exist only during schema resolution. No table, no TypeScript types, no client accessor, and no registry entry are generated for them — they're consumed during inheritance resolution and then discarded.

Syntax

The abstract keyword goes before model. Everything inside the body follows normal field syntax — decorators, optionals, arrays, and relations all work:

abstract model BaseEntity {
  id Record @id
  createdAt Date @createdAt
  updatedAt Date @updatedAt
}

What Abstract Suppresses

An abstract model produces nothing in the generated output:

Generated artifactAbstractConcrete
SurrealDB tableNoYes
TypeScript interfaceNoYes
Client accessor (db.Model)NoYes
Model registry entryNoYes

Concrete children that extend an abstract model get their own tables and types with the inherited fields flattened in. There is no client.db.BaseEntity — the abstract model doesn't exist at runtime.

Basic Usage

Define common fields once and share them across any number of concrete models:

abstract model Timestamped {
  id Record @id
  createdAt Date @createdAt
  updatedAt Date @updatedAt
}

model User extends Timestamped {
  email Email @unique
  name String
}

model Post extends Timestamped {
  title String
  content String?
}

Both User and Post get id, createdAt, and updatedAt from Timestamped. Each model has its own independent table and generated types:

// User has all five fields
const user = await client.db.User.create({
  data: { email: 'alice@test.com', name: 'Alice' },
});
user.id;        // CerialId<string>
user.createdAt; // Date
user.updatedAt; // Date

// Post has its own four fields
const post = await client.db.Post.create({
  data: { title: 'Hello World' },
});

Layered Abstracts

Abstract models can extend other abstract models, building up shared field sets incrementally:

abstract model L1Base {
  id Record @id
  createdAt Date @createdAt
}

abstract model L2Named extends L1Base {
  name String
  description String?
}

abstract model L3Tagged extends L2Named {
  tags String[]
  metadata Int?
}

model Article extends L3Tagged {
  status String @default('draft')
}

Article gets the full chain: id, createdAt, name, description, tags, metadata, and status. Each abstract layer adds its own fields without repeating what came before.

Inheritance Rules

What Can Extend What

ParentChildAllowed?
abstractabstractYes
abstractconcreteYes
concreteconcreteNo
concreteabstractNo

All models — both concrete and abstract — can only extend abstract models. A model cannot extend a concrete model. This keeps a clean separation: abstract models serve as reusable templates, and concrete models are always leaf types with their own tables.

To share fields between concrete models, extract the common fields into an abstract intermediary:

abstract model BaseEntity {
  id Record @id
  createdAt Date @createdAt
  updatedAt Date @updatedAt
}

abstract model BaseUser extends BaseEntity {
  email Email @unique
  name String
  isActive Bool @default(true)
}

model RegularUser extends BaseUser {
  preferences String?
}

model Admin extends BaseUser[!isActive] {
  level Int @default(1)
  permissions String[]
}

Only Models Can Be Abstract

The abstract keyword applies to models only. Objects, tuples, enums, and literals cannot be declared abstract — they don't generate tables, so there's no table to suppress. Any type kind can be used as a parent in extends without needing abstract.

No Directives on Abstract Models

Composite directives (@@index, @@unique) are not allowed on abstract models. Since abstract models don't generate tables, there's nothing to index. If a concrete child needs an index, it must declare its own:

abstract model BaseEntity {
  id Record @id
  name String
  email Email
  // @@unique([name, email])  — not allowed here
}

model User extends BaseEntity {
  age Int?
  @@unique([name, email])  // declare on the concrete model
}

Placing @@index or @@unique on an abstract model is a schema validation error, not a silent no-op.

Common Patterns

Shared Timestamps

The most common use case — define id and timestamp fields once:

abstract model BaseEntity {
  id Record @id !!private
  createdAt Date @createdAt !!private
  updatedAt Date @updatedAt
}

model User extends BaseEntity {
  email Email @unique
  name String
}

model Comment extends BaseEntity {
  content String
  authorId Record
  author Relation @field(authorId) @model(User)
}

Using !!private on id and createdAt prevents children from accidentally redefining those structural fields.

Role Hierarchy

Build specialized models from a common user shape:

abstract model BaseUser extends BaseEntity {
  email Email @unique
  name String
  isActive Bool @default(true)
}

model RegularUser extends BaseUser {
  preferences String?
}

model Admin extends BaseUser[!isActive] {
  level Int @default(1)
  permissions String[]
}

Admin omits isActive since admins are always active by definition. RegularUser inherits the full set.

Abstract models are consumed during inheritance resolution and then discarded. They leave no trace in the generated output — only their fields live on inside the concrete children that extend them.

On this page