Important: Schema file naming
- Schema files must start with a capital letter and be PascalCase, e.g.,
Doctors.json,Hospitals.json.- The file name should match the
modelNamevalue inside the file, e.g.,Doctors.json→"modelName": "Doctors".- Place your schema files in your configured schemas directory (e.g.,
examples/node/schemas/).Following these conventions ensures consistent model registration and avoids case‑sensitive filesystem issues.
Define schemas in @airoom/nextmin-node
This page is a practical, generic guide to write your own schemas. It explains the structure, lists all unique field params supported by the examples, and provides copy‑paste templates.
If you already have working schemas, you can skim to the Field parameters catalog and Full schema template.
Schema structure
A schema is a JSON object with these top‑level keys:
- modelName: string. The collection/model name.
- group: string. Used for grouping in UIs.
- extends: string. Inherit base fields/behavior from another model (e.g. “Users”).
- attributes: object. Field definitions keyed by field name.
- allowedMethods: flags to enable CRUD endpoints.
- access: public/authenticated/roles permissions.
Minimal schema example
{
"modelName": "Posts",
"group": "Content",
"attributes": {
"title": { "type": "string", "required": true },
"slug": { "type": "string", "required": true, "unique": true, "populateSlugFrom": "title" },
"published": { "type": "boolean", "default": false }
},
"allowedMethods": { "create": true, "read": true, "update": true, "delete": true },
"access": {
"public": { "read": true, "create": false, "update": false, "delete": false },
"authenticated": { "read": true, "create": true, "update": false, "delete": false },
"roles": {
"admin": { "read": true, "create": true, "update": true, "delete": true }
}
}
}Full schema template (with all commonly used params)
Use this as a starting point and remove what you don’t need.
{
"modelName": "ExampleModel",
"extends": "Users", // optional: inherit from another model
"group": "ExampleGroup",
"attributes": {
// BASIC FIELD TYPES
"name": { "type": "string", "required": true },
"description": { "type": "string", "longtext": true },
"age": { "type": "number", "min": 0, "max": 120 },
"email": { "type": "email", "required": true, "unique": true },
"isActive": { "type": "boolean", "default": false, "index": true },
// ENUM
"status": { "type": "string", "enum": ["draft", "published", "archived"], "default": "draft" },
// MASKED INPUT (e.g., phone numbers)
"phone": { "type": "number", "mask": "+xxx-xxxx-xxxxxx" },
// RELATIONS (ObjectId)
"owner": { "type": "ObjectId", "ref": "Users", "show": "email" },
"tags": [{ "type": "ObjectId", "ref": "Tags", "show": "name", "pageSize": 100 }],
// SCHEDULE / RANGE FIELDS
"monday": { "type": "range", "timeOnly": true },
// FILE UPLOAD FIELDS (store uploaded file ids/paths as strings)
"image": [{ "type": "string", "fileTypes": "images/*", "maxFilesCount": 1, "maxFileSize": 2097152 }],
// ADDRESS AUTOCOMPLETE (paired field)
"address": { "type": "string", "populate": "latLng", "countryCodes": ["bd"], "limit": 8 },
"latLng": { "type": "string", "hidden": true }
},
"allowedMethods": { "create": true, "read": true, "update": true, "delete": true },
"access": {
"public": { "read": true, "create": false, "update": false, "delete": false },
"authenticated": { "read": true, "create": true, "update": false, "delete": false },
"roles": {
"admin": { "read": true, "create": true, "update": true, "delete": true },
"superadmin": { "read": true, "create": true, "update": true, "delete": true }
}
}
}Field parameters catalog
Below are the unique parameters you can use inside attributes, grouped by purpose.
-
Core
- type: string | number | boolean | email | ObjectId | range | string (file) | string (address)
- required: boolean – value must be present on create/update.
- default: any – default value if not provided.
- hidden: boolean – stored in DB but hidden from general reads/forms.
- longtext: boolean – hints UI to use a textarea/editor for long strings.
- populateSlugFrom: string – comma-separated list of field names to auto-generate a slug from (e.g., “title” or “firstName,lastName”). Automatically enforces
required: true. - rich: boolean – hints UI to use a rich text editor (Tiptap) for this string field. Sanitized on output.
-
Validation & indexing
- enum: string[] – allowed values for string fields.
- min, max: number – numeric bounds (e.g., Ratings.rating).
- unique: boolean – unique index constraint (e.g., Districts.slug).
- index: boolean – create a secondary index (e.g., Doctors.verified).
- mask: string – input mask for number-like fields (e.g., phones).
- minLength, maxLength: number – string length constraints (e.g., phone numbers).
- pattern: string – regex pattern for string validation.
-
Relations (ObjectId)
- ref: string – target model name (e.g., “Hospitals”).
- show: string – which field to display in UI selects (e.g., “name”).
- pageSize: number – page size for relation pickers (e.g., 64, 2000).
- Arrays: wrap the field in
[ { ...params } ]to allow multiple references.
-
Files (stored as strings)
- fileTypes: string – MIME pattern, e.g., “images/*”.
- maxFilesCount: number – limit number of files.
- maxFileSize: number – per-file size limit in bytes (2MB = 2097152).
- Arrays: represent multi-file inputs.
-
Address autocomplete pair
- address field:
{ type: "string", populate: "latLng", countryCodes: ["bd"], limit: 8 } - latLng field:
{ type: "string", hidden: true } - populate: string – name of the hidden field to auto-fill.
- countryCodes: string[] – restrict search to specific countries.
- limit: number – max suggestions.
- address field:
-
Schedule/range
- type: “range”, timeOnly: true – value format
{ start: "HH:mm", end: "HH:mm" }.
- type: “range”, timeOnly: true – value format
-
Repeatable JSON groups
- Use a string field with
format: "json-group"andrepeat: trueto model a repeatable set of sub-fields. - Provide an
attributesobject describing the inner fields. Recursive support allows you to nestjson-groupwithin anotherjson-groupup to any depth. - Provide an
attributesobject describing the inner fields; you can also setminItems/maxItems.
Example:
json{ "chambers": { "type": "string", "format": "json-group", "repeat": true, "attributes": { "chamberName": { "type": "string", "required": true }, "appointmentPhone": { "type": "string", "required": true }, "chamberAddress": { "type": "string", "required": true } }, "minItems": 1, "maxItems": 5 } } - Use a string field with
Advanced Relationship Features
NextMin provides intelligent features to eliminate API waterfalls and improve SSR performance by embedding related data directly into your documents.
Virtual Fields (Inverse Counts)
Virtual fields allow you to automatically fetch the count of documents that reference the current document. These are computed at runtime and are not stored in the database.
{
"attributes": {
"doctorCount": {
"type": "virtual",
"op": "count",
"ref": "Doctors",
"foreignField": "hospitalId",
"localField": "id"
}
}
}Aggregated Summaries
Unlike virtual fields, Aggregated Summaries are physically stored and automatically updated whenever a related document is created, updated, or deleted. This provides the highest performance for large datasets.
{
"modelName": "Hospitals",
"attributes": {
"ratingCount": { "type": "number", "default": 0 },
"ratingAverage": { "type": "number", "default": 0 },
"doctorCount": { "type": "number", "default": 0 }
},
"aggregates": [
{
"op": "count",
"ref": "Doctors",
"foreignField": "hospitalId",
"localField": "id",
"as": "doctorCount"
},
{
"op": "avg",
"ref": "Ratings",
"foreignField": "subjectId",
"localField": "id",
"as": "ratingAverage",
"countField": "ratingCount", // Optional: also updates the count
"valueField": "rating" // The field to average
}
]
}Dynamic References (refPath) & Attribute Immutability (readOnly)
NextMin supports dynamic relation resolution per document using refPath, and strict field immutability using readOnly.
Dynamic References (refPath)
When a reference field can point to different models dynamically depending on another field’s value in the same document (e.g. subjectId pointing to Doctors or Hospitals based on subjectType), set "refPath": "<fieldName>":
{
"modelName": "Ratings",
"attributes": {
"subjectType": {
"type": "string",
"required": true,
"enum": ["Doctors", "Hospitals"],
"readOnly": true
},
"subjectId": {
"type": "ObjectId",
"refPath": "subjectType",
"show": "fullName",
"required": true,
"readOnly": true
}
}
}- Strict Enum Rule: The attribute specified by
refPath(e.g.subjectType) MUST exist in the same schema and define anenumstring array containing the target model names. - Mutual Exclusion:
refPathandrefare mutually exclusive. If both are specified,refPathtakes precedence. - Terminal Developer Warnings: During server initialization,
@airoom/nextmin-nodeautomatically validatesrefPathdefinitions and logs warnings to the terminal if:- The target field specified in
refPathdoes not exist in the same schema. - The target field is not a string attribute with an
enumarray of target model names. - An
enumstring value does not correspond to any registered schema model. - Both
refandrefPathare declared on the same attribute.
- The target field specified in
- Runtime Safety: If a document has a missing/invalid
refPathvalue or unpopulated reference,@airoom/nextmin-nodesafely keeps the raw ID without throwing uncaught exceptions.
Attribute Immutability (readOnly)
Set "readOnly": true or "editable": false on an attribute definition:
- API Level: Update requests (
PUT /:model/:id) automatically stripreadOnlyattributes from the update payload. - Admin UI:
@airoom/nextmin-reactrenders disabled inputs in form views forreadOnlyfields.
Authentication & Registration Configuration (Users Schema Only)
NextMin supports specialized configuration parameters on the Users model (and models extending Users) to control authentication mechanisms, secure registration flows, and implement OTP verification gates.
{
"modelName": "Users",
"authServices": ["email", "username", "phone"],
"phoneVerification": true,
"publicRegisterFields": [
"username",
"email",
"phone",
"password",
"role"
],
"roleDefaultStatuses": {
"buyer": "active",
"developer": "pending",
"agent": "pending",
"user": "active"
}
}authServices:string[]Array of allowed login identifiers. Supports"email","username", and"phone". Defaults to["email", "username"]if omitted. When"phone"is included, users can register and login using their phone numbers.phoneVerification:booleanEnforces SMS OTP verification during registration. When enabled, a valid one-time password (otp) matching the user’sphonemust be supplied to the registration endpoint.- Role-Level Overrides: You can enable or disable phone verification selectively for subclass schemas (e.g., a
Developersschema extendingUserscan set"phoneVerification": trueeven ifUsershas it set tofalse).
- Role-Level Overrides: You can enable or disable phone verification selectively for subclass schemas (e.g., a
publicRegisterFields:string[]Whitelist of fields a public signup client is allowed to submit. If a client attempts to submit administrative fields (likestatusor unknown custom fields) not present in the whitelist, a400 Bad Requestis returned. If omitted, all fields are permitted for backward compatibility.roleDefaultStatuses:Record<string, string>Maps user roles to their default initial status. In multi-role portals, you can enforce that developers and agents register in apendingstate awaiting admin approval, while buyers are activated immediately.
Access and methods
- allowedMethods: enable or disable CRUD endpoints per model:
{
"allowedMethods": { "create": true, "read": true, "update": true, "delete": true }
}- access: define who can do what. You can combine public, authenticated, and granular roles:
{
"access": {
"public": { "read": true, "create": false, "update": false, "delete": false },
"authenticated": { "read": true, "create": true, "update": false, "delete": false },
"roles": {
"admin": { "read": true, "create": true, "update": true, "delete": true },
"editor": { "read": true, "create": true, "update": true, "delete": false }
}
}
}Notes:
- Public means unauthenticated requests.
- Roles are additive; if multiple apply, the most permissive per action wins.
Deep Access Merging
When extending a base model (or loading schemas dynamically), NextMin performs a deep merge on the access block. This guarantees that fields in child schemas (like custom role overrides, default values, or local restrictions) merge into the base model’s default access rules, rather than overriding the entire block. This prevents security leaks where child schemas accidentally nullify base authentication policies.
Sequential Role-Based & Owner Permissions
NextMin implements a context-aware ownership evaluation system:
- Dynamic
"owner"Ownership: When checking ownership rules, the system determines the relationship dynamically based on context:- For standard users, ownership matches their own record (
document.id === currentUserId). - For administrators (admin role), ownership resolves to their own record plus any records they created (
document.createdBy === currentUserId).
- For standard users, ownership matches their own record (
- Owner-Scoped Query Filtering: When an administrator requests a list of records under an
"owner"policy, NextMin automatically applies anownerFilterreturning{ or: [{ id: currentUserId }, { createdBy: currentUserId }] }. This allows them to see and manage only their own accounts and users they created. - Superadmin Global Bypass: The
superadminrole possesses global bypass permissions, allowing it to view, update, or delete all records (including system/administrative records) without local owner-scoping filters.
Role Escalation Guards
To prevent privilege escalation during user registration and creation:
- Admin Assignment Protection: Standard administrators and public registrants are explicitly forbidden from assigning high-privilege roles (like
adminorsuperadmin) to new users. - Role Hierarchy Enforcement: The server validates assigned roles asynchronously against role hierarchies. Any attempt to register or update a user with a role equal to or higher than the current user’s role (unless initiated by a
superadmin) will be rejected with a403 Forbiddenerror.
Extending a base model (inheritance)
You can inherit from another model by adding an extends key. A common pattern is Doctors extending Users.
Example Doctors schema:
{
"modelName": "Doctors",
"extends": "Users",
"group": "Doctors",
"attributes": {
"title": { "type": "string", "required": true },
"profilePicture": [{ "type": "string", "fileTypes": "images/*", "maxFilesCount": 1, "maxFileSize": 2097152, "required": true }],
"phone": { "type": "number", "mask": "+xxx-xxxx-xxxxxx", "required": true },
"aboutYou": { "type": "string", "longtext": true, "required": true },
"position": { "type": "string", "required": true },
"degree": { "type": "string", "required": true },
"experienceYears": { "type": "number", "required": true },
"licenseNumber": { "type": "string", "required": true },
"video": { "type": "string" },
"verified": { "type": "boolean", "default": false, "index": true }
},
"allowedMethods": { "create": true, "read": true, "update": true, "delete": true }
}Runtime behavior (server):
- The child collection stores a private
baseIdlinking to its base record; this field is never exposed in schema output or list/detail responses. - Inherited Field Stripping (
stripInheritedIfNoBase): When an extended record exists without a linked base user (baseIdis null or empty), inherited user fields (likeusername,password,email) are automatically excluded from the JSON response to keep API output clean. - Intelligent Base Creation: During creation, a base user is only created if meaningful user fields are explicitly provided in the payload (ignoring auto-injected defaults like
status,role, ortype), preventing orphaned user accounts. - Create: POST /rest/doctors can include both Users fields (e.g., email, username, password) and Doctors fields. The server splits them, creates Users, then creates Doctors with
baseIdset automatically. You do not need to sendbaseId. - Read: GET /rest/doctors and GET /rest/doctors/:id return merged documents: Users + Doctors. If a field exists on both, Doctors wins.
- Update: PUT /rest/doctors/:id may include Users and/or Doctors fields. The server partitions and updates both sides, then returns the merged doc. Changing
baseIdis not allowed. - Delete: Deleting a Doctors record also deletes its linked Users record.
- Validation: Required checks apply to the model you’re touching (Doctors when creating Doctors) and skip private fields. Unique checks run per collection.
UI behavior (@airoom/nextmin-react):
- The SchemaForm hides
baseIdeven if policies would expose private fields. When editing an existing record, the hiddenbaseIdvalue is preserved so updates work like a foreign key. - The DynamicViewer Drawer and tables automatically omit inherited User fields if the document is not linked to a base account.
Custom Actions Configuration (customActions)
You can define custom row-level actions directly in your schema definition:
{
"modelName": "Doctors",
"customActions": [
{
"name": "qr",
"label": "QR Code",
"action": "qr",
"urlTemplate": "https://yourdomain.com/doctor/{slug}"
},
{
"name": "viewWebsite",
"label": "View Live",
"href": "https://yourdomain.com/doctor/{slug}"
}
]
}action: "qr": Automatically renders a QR Code button openingQRCodeModal, interpolating dynamic fields like{slug}or{id}into destination URLs, offering instant SVG and 1024px PNG downloads.isSystemRowProtection: Individual documents or schemas can mark baseline system records to automatically disable Delete and Edit actions in@airoom/nextmin-reactdata tables.
Tips for designing your schema
- Prefer string IDs for files; use the file upload API to get permanent paths/IDs.
- For relations, always set both ref and show to get nice selects in UIs.
- Use unique for slugs or any natural key, and index for frequently filtered fields.
- Pair address + latLng as shown to enable geocoding while keeping coordinates hidden.
- If you need user-like fields, consider extends: “Users” as in Doctors.
- Keep pageSize modest (e.g., 64) for better performance in dropdowns.
That’s it. With the template and parameter catalog above, you have all you need to create your own schema from scratch.