CLI Configuration
The .apsorc file is a JSON configuration file that describes your entire data model. It is the single source of truth that the Apso CLI reads to generate your backend code.
File Location
Place the .apsorc file in the root of your project directory (the same directory where you run apso server scaffold). The CLI uses the rc library to locate the file, searching upward from the current directory through parent directories.
my-api/
├── .apsorc <-- here
├── package.json
├── src/
│ ├── autogen/
│ └── extensions/
└── ...Version 2 Format
The current schema format is version 2. Always set "version": 2 at the top of your .apsorc file. Version 1 is a legacy format with limited features and is not recommended for new projects.
Top-Level Structure
{
"version": 2,
"rootFolder": "src",
"language": "typescript",
"apiType": "Rest",
"auth": { ... },
"entities": [ ... ],
"relationships": [ ... ]
}| Field | Type | Required | Description |
|---|---|---|---|
version | integer | Yes | Must be 2 |
rootFolder | string | Yes | Directory where generated source code is placed (e.g., "src") |
language | string | No | Default target language: "typescript", "python", or "go". Can be overridden by the --language CLI flag. |
apiType | string | No | "Rest" (default) or "Graphql". GraphQL requires version 2 and TypeScript. |
auth | object | No | Authentication provider configuration. See Authentication. |
entities | array | Yes | Array of entity definitions |
relationships | array | Yes | Array of relationship definitions between entities |
Entities
Each entity represents a database table and generates a complete set of backend files (entity class, DTO, service, controller, module).
{
"entities": [
{
"name": "Project",
"created_at": true,
"updated_at": true,
"primaryKeyType": "serial",
"fields": [
{ "name": "name", "type": "text", "length": 255 },
{ "name": "status", "type": "enum", "values": ["Active", "Archived"], "default": "Active" },
{ "name": "description", "type": "text", "nullable": true }
]
}
]
}Entity Properties
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Entity name in PascalCase. Becomes the class name and (lowercased) the table name. |
fields | array | Yes | — | Array of field definitions (at least one required) |
created_at | boolean | No | false | Add a created_at timestamp column with @CreateDateColumn() |
updated_at | boolean | No | false | Add an updated_at timestamp column with @UpdateDateColumn() |
primaryKeyType | string | No | "serial" | Primary key type: "serial" (auto-increment integer) or "uuid" |
uniques | array | No | — | Multi-field unique constraints |
scopeBy | string | string[] | No | — | Fields for multi-tenant data isolation. See Data Scoping. |
scopeOptions | object | No | — | Fine-tune scope enforcement behavior |
Auto-Generated Columns
Every entity automatically receives these columns (you do not need to define them in fields):
id— primary key (@PrimaryGeneratedColumn()or@PrimaryGeneratedColumn("uuid"))created_at— if"created_at": trueis setupdated_at— if"updated_at": trueis set- Foreign key columns — generated automatically from relationships (e.g.,
workspaceId)
Fields
Each field maps to a database column with appropriate TypeORM decorators and validation rules.
{
"name": "email",
"type": "text",
"length": 255,
"unique": true,
"is_email": true
}Field Properties
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Column name (camelCase recommended) |
type | string | Yes | Data type (see table below) |
unique | boolean | No | Add a unique constraint |
nullable | boolean | No | Allow null values. Adds @IsOptional() validator. |
length | integer | No | Maximum string length. Adds @MaxLength() validator. |
default | string | number | boolean | No | Default column value |
is_email | boolean | No | Add @IsEmail() validation |
values | string[] | Required for enum | Allowed values for enum fields |
precision | integer | No | Total digits for decimal/numeric types |
scale | integer | No | Digits after decimal point for decimal/numeric types |
Supported Field Types
| Type | PostgreSQL Type | TypeORM Decorator | Auto Validation |
|---|---|---|---|
text | text | @Column({ type: 'text' }) | @IsString(), @IsNotEmpty() |
integer | integer | @Column({ type: 'integer' }) | @IsNumber() |
float | float | @Column({ type: 'float' }) | @IsNumber() |
decimal | decimal(p,s) | @Column({ type: 'decimal', precision, scale }) | @IsNumber() |
numeric | numeric(p,s) | @Column({ type: 'numeric', precision, scale }) | @IsNumber() |
boolean | boolean | @Column({ type: 'boolean' }) | @IsBoolean() |
enum | enum | @Column({ type: 'enum', enum: [...] }) | — |
json | jsonb | @Column('jsonb') | — |
date | date | @Column({ type: 'date' }) | — |
timestamptz | timestamptz | @Column({ type: 'timestamptz' }) | — |
point | point | @Column({ type: 'point' }) | — (requires PostGIS) |
linestring | linestring | @Column({ type: 'linestring' }) | — (requires PostGIS) |
polygon | polygon | @Column({ type: 'polygon' }) | — (requires PostGIS) |
multipoint | multipoint | @Column({ type: 'multipoint' }) | — (requires PostGIS) |
multilinestring | multilinestring | @Column({ type: 'multilinestring' }) | — (requires PostGIS) |
multipolygon | multipolygon | @Column({ type: 'multipolygon' }) | — (requires PostGIS) |
geometry | geometry | @Column({ type: 'geometry' }) | — (requires PostGIS) |
geography | geography | @Column({ type: 'geography' }) | — (requires PostGIS) |
geometrycollection | geometrycollection | @Column({ type: 'geometrycollection' }) | — (requires PostGIS) |
PostGIS types require the PostGIS extension installed in your PostgreSQL database:
CREATE EXTENSION IF NOT EXISTS postgis;
Field Examples
Text with length and email validation:
{ "name": "email", "type": "text", "length": 255, "is_email": true, "unique": true }Enum with default:
{ "name": "role", "type": "enum", "values": ["User", "Admin", "Owner"], "default": "User" }Decimal with precision:
{ "name": "price", "type": "decimal", "precision": 10, "scale": 2, "default": 0 }Nullable JSON:
{ "name": "metadata", "type": "json", "nullable": true }Relationships
Relationships are defined in a separate top-level array. Each relationship generates the appropriate TypeORM decorators, foreign key columns, and join tables.
Important: Define only ONE side of each relationship. Apso auto-generates the inverse side. Defining both sides causes duplicate properties and compilation errors.
{
"relationships": [
{ "from": "Workspace", "to": "User", "type": "ManyToOne", "to_name": "owner" },
{ "from": "Project", "to": "Workspace", "type": "ManyToOne" },
{ "from": "Workspace", "to": "Project", "type": "OneToMany" }
]
}Relationship Properties
| Property | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source entity name |
to | string | Yes | Target entity name |
type | string | Yes | OneToMany, ManyToOne, ManyToMany, or OneToOne |
to_name | string | No | Custom property name on the source entity for the relationship (e.g., "owner" instead of the default "user") |
nullable | boolean | No | Whether the foreign key is nullable |
cascadeDelete | boolean | No | Cascade delete related entities when the source is deleted |
bi_directional | boolean | No | Generate both sides of a ManyToMany relationship |
joinTableName | string | No | Custom join table name (ManyToMany only) |
joinColumnName | string | No | Custom foreign key column name in join table for the from entity (ManyToMany only) |
inverseJoinColumnName | string | No | Custom foreign key column name in join table for the to entity (ManyToMany only) |
Relationship Examples
ManyToOne with custom name:
{ "from": "Project", "to": "User", "type": "ManyToOne", "to_name": "createdBy", "nullable": true }Generates on the Project entity:
@ManyToOne(() => User)
@JoinColumn({ name: 'createdById' })
createdBy: User;
@Column({ type: 'integer', nullable: true })
createdById: number;OneToMany:
{ "from": "User", "to": "Project", "type": "OneToMany" }Generates on the User entity:
@OneToMany(() => Project, (project) => project.user)
projects: Project[];ManyToMany:
{ "from": "User", "to": "Role", "type": "ManyToMany", "to_name": "roles" }Generates:
@ManyToMany(() => Role, (role) => role.users)
@JoinTable()
roles: Role[];Self-referencing relationship:
{ "from": "Category", "to": "Category", "type": "ManyToOne", "to_name": "parent", "nullable": true }Multi-Field Unique Constraints
Define unique constraints that span multiple columns:
{
"name": "WorkspaceUser",
"fields": [
{ "name": "email", "type": "text" },
{ "name": "role", "type": "enum", "values": ["User", "Admin"] }
],
"uniques": [
{ "name": "UQ_workspace_user_email", "fields": ["email", "workspaceId"] }
]
}Scope Configuration
The scopeBy and scopeOptions properties on entities control multi-tenant data isolation. When configured, the CLI generates NestJS guards that automatically filter, inject, and verify scope values on every request.
{
"name": "Project",
"scopeBy": "workspaceId",
"scopeOptions": {
"injectOnCreate": true,
"enforceOn": ["find", "get", "create", "update", "delete"],
"bypassRoles": ["superadmin"]
},
"fields": [
{ "name": "name", "type": "text" }
]
}| scopeOptions Property | Type | Default | Description |
|---|---|---|---|
injectOnCreate | boolean | true | Auto-inject scope value on POST requests |
enforceOn | string[] | All operations | Which operations enforce scope: find, get, create, update, delete |
bypassRoles | string[] | [] | Roles that skip scope checking |
For full details on authentication and scoping, see the Authentication documentation.
Complete Example
{
"version": 2,
"rootFolder": "src",
"language": "typescript",
"apiType": "Rest",
"auth": {
"provider": "better-auth",
"sessionEntity": "session",
"userEntity": "User",
"accountUserEntity": "AccountUser",
"organizationField": "organizationId"
},
"entities": [
{
"name": "User",
"created_at": true,
"updated_at": true,
"fields": [
{ "name": "email", "type": "text", "length": 255, "is_email": true, "unique": true },
{ "name": "fullName", "type": "text", "nullable": true }
]
},
{
"name": "Workspace",
"created_at": true,
"updated_at": true,
"fields": [
{ "name": "name", "type": "text" }
]
},
{
"name": "WorkspaceUser",
"created_at": true,
"updated_at": true,
"fields": [
{ "name": "email", "type": "text", "length": 255, "is_email": true },
{ "name": "invite_code", "type": "text", "length": 64 },
{ "name": "role", "type": "enum", "values": ["User", "Admin"], "default": "Admin" },
{ "name": "status", "type": "enum", "values": ["Active", "Invited", "Inactive", "Deleted"] },
{ "name": "activeAt", "type": "date", "nullable": true }
]
},
{
"name": "Project",
"created_at": true,
"updated_at": true,
"scopeBy": "workspaceId",
"fields": [
{ "name": "name", "type": "text" },
{ "name": "status", "type": "enum", "values": ["Active", "Archived"], "default": "Active" }
]
}
],
"relationships": [
{ "from": "User", "to": "WorkspaceUser", "type": "OneToMany", "nullable": true },
{ "from": "Workspace", "to": "WorkspaceUser", "type": "OneToMany" },
{ "from": "Workspace", "to": "Project", "type": "OneToMany" },
{ "from": "Project", "to": "User", "type": "ManyToOne", "to_name": "owner" }
]
}Schema Validation
The .apsorc file is validated against a JSON Schema (apsorc.schema.json) included with the CLI package. Key validation rules:
versionmust be2- Every entity must have a
nameand at least onefieldsentry - Every field must have a
nameandtype - Enum fields must include a
valuesarray - Relationship
typemust be one ofOneToMany,ManyToOne,ManyToMany,OneToOne - Relationship
fromandtomust reference entity names defined in theentitiesarray