# AdminJS

{% hint style="info" %}
AdminJS is an open-source admin panel for your Node.js application. It does not force it's database schema upon your application; instead, it integrates with your Node.js server and the ORM/ODM you are using.\
\
The UI components are written in React and you can fully customize the admin panel.\
AdminJS also generates it's own REST API which you can use outside of the admin panel or use it to integrate it with your other applications.
{% endhint %}

![AdminJS Demo](/files/O1K6hhJzUZr9wFT8AJ6I)

{% hint style="info" %}
Visit our [demo](https://demo.adminjs.co) for a live preview.\
\
&#x20;`Email: admin@example.com`\
&#x20;`Password: password`
{% endhint %}

An AdminJS panel can be easily integrated with your existing Node.js application due to a large number of supported frameworks:

* Express.js (via `@adminjs/express`)
* Nest.js (via `@adminjs/nestjs`)
* Hapi (via `@adminjs/hapi`)
* Koa (via `@adminjs/koa`)
* Fastify (via `@adminjs/fastify`)

AdminJS also does not force it's own database schema upon you. Instead, it supports a number of ORMs and ODMs to connect with your existing database:

* TypeORM (via `@adminjs/typeorm`)
* Sequelize (via `@adminjs/sequelize`)
* Mongoose (via `@adminjs/mongoose`)
* Prisma (via `@adminjs/prisma`)
* MikroORM (via `@adminjs/mikroorm`)
* Objection (via `@adminjs/objection`)

You may also connect directly to your SQL (currently only Postgres) database via `@adminjs/sql`.

At the same time, you can fully customize the look of AdminJS panel. The user interface is built with React, similarly you can write your new React custom components to add new UI elements or override the existing ones.

{% hint style="info" %}
Visit our [homepage](https://adminjs.co/) to find out more information about AdminJS and read our [case studies](https://adminjs.co/enterprise#case-studies).
{% endhint %}


# Getting started

The "Getting started" section is an overview and quick explanation of AdminJS ecosystem. Detailed instructions for setting up your AdminJS application and connecting it to your database(s) are described in separate guides (scroll down to [Setup](#setup) for quick links).

### Overview

An AdminJS application consists of:

* a core package
* a plugin (for a framework of your choice)
* an adapter for (for a ORM/ODM of your choice)

### ESM & CJS Support

As of version 7, AdminJS only supports ESM and will no longer work with CommonJS syntax. When setting up your project you should follow the guidelines from Node.js documentation: <https://nodejs.org/docs/latest-v18.x/api/esm.html>

In the most basic scenario, the steps to get started with ESM are:

* setting `type` to `"module"` in your `package.json` file,
* using `import`/`export` syntax with explicit `.js` extensions instead of `require`s,
* if using Typescript, you should set `moduleResolution` and `module` to `"nodenext"` and `target` to `"esnext"` in your `tsconfig.json`

{% hint style="warning" %}
If you use `@adminjs/nestjs` do not update to ESM as NestJS doesn't support ESM as of 2023/04. Instead, please see the [updated guide for NestJS plugin](/installation/plugins/nest) so that you can import updated AdminJS packages into your CJS NestJS app.
{% endhint %}

### Quickstart

{% hint style="info" %}
AdonisJS (and Lucid) integration should be installed using Adonis CLI. Please refer to [Adonis plugin documentation ](/installation/plugins/adonis)to see a thorough guide.
{% endhint %}

`@adminjs/cli` provides `adminjs create` command which you can use to quickly create your AdminJS application.

#### Installation

**NPM:**

```bash
$ npm i -g @adminjs/cli
```

**Yarn:**

```bash
$ yarn global add @adminjs/cli
```

#### Usage

```bash
$ adminjs create
```

You may also set up your AdminJS application manually by following the rest of this documentation.

### Packages

First of all, install the core package:

```bash
$ yarn add adminjs
```

Next, install one of our plugins:

```bash
$ yarn add @adminjs/express                # for Express server
$ yarn add @adminjs/nestjs                 # for Nest server
$ yarn add @adminjs/hapi                   # for Hapi server
$ yarn add @adminjs/koa                    # for Koa server
$ yarn add @adminjs/fastify                # for Fastify server
```

Finally, install an adapter:

```bash
$ yarn add @adminjs/typeorm                # for TypeORM
$ yarn add @adminjs/sequelize              # for Sequelize
$ yarn add @adminjs/mongoose               # for Mongoose
$ yarn add @adminjs/prisma                 # for Prisma
$ yarn add @adminjs/mikroorm               # for MikroORM
$ yarn add @adminjs/objection              # for Objection
$ yarn add @adminjs/sql                    # for raw SQL, currently supports only Postgres
```

### Setup

After you have installed AdminJS dependencies, proceed to:

* [Plugins](/installation/plugins) section for instructions on how to setup AdminJS with your framework.
* [Adapters](/installation/adapters) section for instructions on how to connect your AdminJS instance to your database.

### Frontend bundling

AdminJS needs to generate its own frontend. In the production environment, you would bundle all frontend files during build step of your deployment process, but that would be quite annoying to do in development.

For this reason you should add `adminJS.watch()` function call after setting up all plugins and adapters. This only affects development environment (`process.env.NODE_ENV === 'development'`) and launches a separate bundling process in the background.

```typescript
import AdminJS from 'adminjs'

const adminJS = new AdminJS({
    // ...
})

adminJS.watch()
```

Without this step in development, AdminJS will start but won't display anything useful in the browser (except for some parsing errors in the console).


# Plugins

This section contains detailed instructions on how to setup a simple AdminJS panel using one of the frameworks listed below.

{% content-ref url="/pages/7isVo8c9ARn6ZdHFJTQp" %}
[Express](/installation/plugins/express)
{% endcontent-ref %}

{% content-ref url="/pages/bQyW4CzNIXcMhC24zMOW" %}
[Nest](/installation/plugins/nest)
{% endcontent-ref %}

{% content-ref url="/pages/B6I3f7D9pnrRVk4IcvR4" %}
[Fastify](/installation/plugins/fastify)
{% endcontent-ref %}

{% content-ref url="/pages/hRf74e9RsaHRcNlCTHT0" %}
[Hapi](/installation/plugins/hapi)
{% endcontent-ref %}

{% content-ref url="/pages/Vfvo7RvFVy0SzcJBAIud" %}
[Koa](/installation/plugins/koa)
{% endcontent-ref %}

{% content-ref url="/pages/5EJy5BsvgIZpZv9pWo78" %}
[Community Plugins](/installation/plugins/community-plugins)
{% endcontent-ref %}


# Adonis

@adminjs/adonis

`@adminjs/adonis` is an official integration with [AdonisJS](https://adonisjs.com/) and it's Lucid ORM. It supports AdonisJS v6+ and requires NodeJS v20.6.x+.

{% hint style="warning" %}
`@adminjs/adonis` currently only supports Lucid as it's ORM.  We will soon add support for other database adapters.
{% endhint %}

The easiest way to get started with AdonisJS integration is to follow their [official documentation](https://docs.adonisjs.com/guides/installation).&#x20;

Below you will find a simplified list of steps required to set up your AdminJS panel.

First of all, create your AdonisJS app if you haven't got one yet:

```bash
$ npm init adonisjs@latest -- -K=slim
```

Follow the steps from AdonisJS CLI to configure your application and once it's installed, navigate to the newly created directory.

```bash
$ cd <your_app_directory>
```

AdminJS requires you to install and configure `@adonisjs/session` and `@adonisjs/lucid`

```bash
$ npm install @adonisjs/session @adonisjs/lucid
$ node ace configure @adonisjs/session
$ node ace configure @adonisjs/lucid
```

This will create a configuration file for `@adonisjs/session` in  `config/session.ts`. The default configuration will allow you to sign into your admin panel in your development environment, but before you deploy to production, please see the [official documentation](https://docs.adonisjs.com/guides/session#session).

You should also configure Lucid in `config/database.ts`. Without this, you won't be able to start your app.

You will also need to install the core package of AdminJS:

```bash
$ npm install adminjs
```

Finally, install `@adminjs/adonis` which will add AdminJS integration into your application.

```bash
$ npm install @adminjs/adonis
$ node ace configure @adminjs/adonis
```

Three new files should appear in your codebase.

* **config/adminjs.ts** contains the configuration of Lucid adapter, Authentication and AdminJS
* **app/admin/component\_loader.ts** is a file where [ComponentLoader](/ui-customization/writing-your-own-components) is instantiated and exported
* **app/admin/auth.ts** is a file where default authentication provider is created. Do note that by default, it will let everyone into your admin panel, so make sure you modify the `authenticate` method.

You should be able to start the application with `npm run dev` command and you should see the login page of AdminJS if you navigate to the default URL: [http://localhost:3333/admin](http://localhost:3333/admin/login)

### Authentication

Authentication can be configured in `config/adminjs.ts` under the `auth` option.

Default configuration:

```typescript
import authProvider from '../app/admin/auth.js'

// ...
auth: {
  enabled: true,
  provider: authProvider,
  middlewares: [],
}
```

The authentication can be disabled or enabled using the `enabled` flag.

[Provider](/basics/authentication) contains the logic for authenticating the users in your admin panel. The default provider uses `email` and `password` to authenticate users.

You may also configure any additional `middlewares` to routes that require authentication. Every custom middleware is run after the user's session is confirmed to exist.

### AdminJS Configuration

AdminJS can be configured in `config/adminjs.ts` under the `adminjs` option. It expects you to provide [AdminJSOptions](https://github.com/SoftwareBrothers/adminjs/blob/master/src/adminjs-options.interface.ts) object with the exception of `databases` which cannot be configured with `@adminjs/adonis` and you will have to configure every resource manually.

Resources configuration will be described in more detail in [Lucid ](#lucid)section.

```typescript
adminjs: {
  rootPath: '/admin',
  loginPath: '/admin/login',
  logoutPath: '/admin/logout',
  componentLoader,
  resources: [],
  pages: {},
  locale: {
    availableLanguages: ['en'],
    language: 'en',
    translations: {
      en: {
        actions: {},
        messages: {},
        labels: {},
        buttons: {},
        properties: {},
        components: {},
        pages: {},
        ExampleResource: {
          actions: {},
          messages: {},
          labels: {},
          buttons: {},
          properties: {},
        },
      },
    },
  },
  branding: {
    companyName: 'AdminJS',
    theme: {},
  },
  settings: {
    defaultPerPage: 10,
  },
}
```

`adminjs` comes with some options already preconfigured. Note that `ExampleResource` in `translations` is just an example of how you would configure translations scoped to a specific resource. If your application has a Lucid `User` model that corresponds to `users` table, then `ExampleResource` could be renamed to `users`.

### Middlewares

Middlewares that will be applied to public routes can be configured in `config/adminjs.ts` under the `middlewares` option.

### Lucid

In order to add Lucid resources into your admin panel, you will first have to enable the adapter in `config/adminjs.ts`.

```typescript
adapter: {
  enabled: true
}
```

After you enable the adapter, you can start adding your Lucid models as resources to `adminjs#resources` in `config/adminjs.ts`:

```typescript
import { LucidResource } from '@adminjs/adonis'

import User from '../app/models/user.js'
import Profile from '../app/models/profile.js'

// ...
adminjs: {
  resources: [
    new LucidResource(User, 'postgres'),
    {
      resource: new LucidResource(Profile, 'postgres'),
      options: {},
    }
  ],
  // ...
}
```

**LucidResource** is an additional wrapper which uses Knex to fetch schema information about your database table. This is required to properly type and configure AdminJS resources. For every resource that you configure, it will query the database to get columns metadata. This is donly only once when you start your application.

**LucidResource** takes two arguments in it's constructor. The first is your Lucid model, and the second is your database connection name (this can be checked in `config/database.ts`).

There are two approaches to adding resources. You can either simply pass `new LucidResource(...)` and use the default configuration for that model, or you can pass a `{ resource: new LucidResource(...), options: {}` object with your custom [Resource configuration](https://github.com/SoftwareBrothers/adminjs/blob/master/src/backend/decorators/resource/resource-options.interface.ts#L48) in `options`.


# Express

@adminjs/express

{% hint style="info" %}
Make sure you have installed AdminJS packages described in [Getting started](/installation/getting-started) article.

```bash
$ yarn add adminjs @adminjs/express
```

{% endhint %}

To setup AdminJS panel with Express.js you need to have `express` installed and required peer dependencies:

```bash
$ yarn add express tslib express-formidable express-session
```

Afterwards, follow one of the examples below.

### Simple

{% tabs %}
{% tab title="Javascript" %}
{% code title="app.js" %}

```javascript
import AdminJS from 'adminjs'
import AdminJSExpress from '@adminjs/express'
import express from 'express'

const PORT = 3000

const start = async () => {
  const app = express()

  const admin = new AdminJS({})

  const adminRouter = AdminJSExpress.buildRouter(admin)
  app.use(admin.options.rootPath, adminRouter)

  app.listen(PORT, () => {
    console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`)
  })
}

start()
```

{% endcode %}
{% endtab %}

{% tab title="Typescript" %}
Install Express types:

```bash
$ yarn add -D @types/express
```

{% code title="app.ts" %}

```typescript
import AdminJS from 'adminjs'
import AdminJSExpress from '@adminjs/express'
import express from 'express'

const PORT = 3000

const start = async () => {
  const app = express()

  const admin = new AdminJS({})

  const adminRouter = AdminJSExpress.buildRouter(admin)
  app.use(admin.options.rootPath, adminRouter)

  app.listen(PORT, () => {
    console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`)
  })
}

start()
```

{% endcode %}

\
Now you can start your AdminJS application.
{% endtab %}
{% endtabs %}

### Authenticated

To add authentication, you must use `AdminJSExpress.buildAuthenticatedRouter` instead of `AdminJSExpress.buildRouter`. Additionally, we must set up a session store to keep our session information. In the example below we will store our session in a Postgres table, we will also use `connect-pg-simple` to allow our session store to connect to the database.

{% tabs %}
{% tab title="Javascript" %}
Install additional dependencies:

```bash
$ yarn add connect-pg-simple
```

{% code title="app.js" %}

```javascript
import AdminJS from 'adminjs'
import AdminJSExpress from '@adminjs/express'
import express from 'express'
import Connect from 'connect-pg-simple'
import session from 'express-session'

const PORT = 3000

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email, password) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

const start = async () => {
  const app = express()

  const admin = new AdminJS({})

  const ConnectSession = Connect(session)
  const sessionStore = new ConnectSession({
    conObject: {
      connectionString: 'postgres://adminjs:@localhost:5432/adminjs',
      ssl: process.env.NODE_ENV === 'production',
    },
    tableName: 'session',
    createTableIfMissing: true,
  })

  const adminRouter = AdminJSExpress.buildAuthenticatedRouter(
    admin,
    {
      authenticate,
      cookieName: 'adminjs',
      cookiePassword: 'sessionsecret',
    },
    null,
    {
      store: sessionStore,
      resave: true,
      saveUninitialized: true,
      secret: 'sessionsecret',
      cookie: {
        httpOnly: process.env.NODE_ENV === 'production',
        secure: process.env.NODE_ENV === 'production',
      },
      name: 'adminjs',
    }
  )
  app.use(admin.options.rootPath, adminRouter)

  app.listen(PORT, () => {
    console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`)
  })
}

start()
```

{% endcode %}

As you may have noticed, the `authenticate` function compares credentials you submit in the form with a hardcoded `DEFAULT_ADMIN` object. In your case, you might want to modify `authenticate` function's logic to compare form credentials against real database objects.

{% hint style="info" %}
If you plan to copy-paste this example, make sure you set the database connection string to a functional one.
{% endhint %}
{% endtab %}

{% tab title="Typescript" %}
Install additional dependencies:

```bash
$ yarn add connect-pg-simple
$ yarn add -D @types/connect-pg-simple @types/express-session @types/express
```

{% code title="app.ts" %}

```typescript
import AdminJS from 'adminjs'
import AdminJSExpress from '@adminjs/express'
import express from 'express'
import Connect from 'connect-pg-simple'
import session from 'express-session'

const PORT = 3000

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

const start = async () => {
  const app = express()

  const admin = new AdminJS({})

  const ConnectSession = Connect(session)
  const sessionStore = new ConnectSession({
    conObject: {
      connectionString: 'postgres://adminjs:@localhost:5432/adminjs',
      ssl: process.env.NODE_ENV === 'production',
    },
    tableName: 'session',
    createTableIfMissing: true,
  })

  const adminRouter = AdminJSExpress.buildAuthenticatedRouter(
    admin,
    {
      authenticate,
      cookieName: 'adminjs',
      cookiePassword: 'sessionsecret',
    },
    null,
    {
      store: sessionStore,
      resave: true,
      saveUninitialized: true,
      secret: 'sessionsecret',
      cookie: {
        httpOnly: process.env.NODE_ENV === 'production',
        secure: process.env.NODE_ENV === 'production',
      },
      name: 'adminjs',
    }
  )
  app.use(admin.options.rootPath, adminRouter)

  app.listen(PORT, () => {
    console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`)
  })
}

start()
```

{% endcode %}

As you may have noticed, the `authenticate` function compares credentials you submit in the form with a hardcoded `DEFAULT_ADMIN` object. In your case, you might want to modify `authenticate` function's logic to compare form credentials against real database objects.

{% hint style="info" %}
If you plan to copy-paste this example, make sure you set the database connection string to a functional one.
{% endhint %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
For a more complex example, please visit our [example app](https://github.com/SoftwareBrothers/adminjs-example-app/blob/master/src/servers/express/index.ts).
{% endhint %}


# Nest

@adminjs/nestjs

{% hint style="warning" %}
AdminJS is now ESM-only. If you plan to use NestJS in your project, NestJS doesn't work with ESM out of the box and you should be aware that you can run into issues. We suggest to use `@adminjs/express` instead. This guide will show you how you can set up the latest AdminJS version with NestJS but do note that this is just a workaround until NestJS maintainers decide to move to or at least support ESM.
{% endhint %}

{% hint style="info" %}
Example repository with AdminJS v7 working with NestJS & ESM:\
<https://github.com/dziraf/adminjs-v7-with-nestjs>
{% endhint %}

{% hint style="info" %}
Make sure you have installed AdminJS packages described in [Getting started](/installation/getting-started) article.

```bash
$ yarn add adminjs @adminjs/nestjs
```

{% endhint %}

As of version `5.x.x` of `@adminjs/nestjs`, the plugin only supports Nest servers that use Express. Fastify Nest servers are not currently supported.

{% hint style="info" %}
If you are starting a new project, install `nest-cli` and bootstrap the project:

```bash
$ npm i -g @nestjs/cli
$ nest new
```

{% endhint %}

`@adminjs/nestjs` uses `@adminjs/express` to set up AdminJS, because of this you have to additionally install it's dependencies:

```bash
$ yarn add @adminjs/express express-session express-formidable
```

Set `moduleResolution` to `nodenext` or `node16` in your `tsconfig.json`:

```json
{
  "compilerOptions": {
    "moduleResolution": "node16",
    "module": "commonjs",
    "target": "esnext",
    // ...
  }
}
```

Next, add Nest plugin code into `imports` of your `AppModule`:

{% code title="app.module.ts" %}

```typescript
import { Module } from '@nestjs/common'

import { AppController } from './app.controller'
import { AppService } from './app.service'

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

@Module({
  imports: [
    // AdminJS version 7 is ESM-only. In order to import it, you have to use dynamic imports.
    import('@adminjs/nestjs').then(({ AdminModule }) => AdminModule.createAdminAsync({
      useFactory: () => ({
        adminJsOptions: {
          rootPath: '/admin',
          resources: [],
        },
        auth: {
          authenticate,
          cookieName: 'adminjs',
          cookiePassword: 'secret'
        },
        sessionOptions: {
          resave: true,
          saveUninitialized: true,
          secret: 'secret'
        },
      }),
    })),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

{% endcode %}

Now you should be able to start your AdminJS application:

```bash
$ nest start
```

AdminJS panel will be available under `http://localhost:3000/admin` if you have followed this example thoroughly.

### How to remove authentication?

If you don't want your admin panel to require authentication, simply follow the example above, but remove `auth` and `sessionOptions` configuration, for example:

```typescript
import('@adminjs/nestjs').then(({ AdminModule }) => AdminModule.createAdminAsync({
  useFactory: () => ({
    adminJsOptions: {
      rootPath: '/admin',
      resources: [],
    },
  }),
})),
```

### Using AdminJS ESM packages

Since AdminJS version 7 and it's compatible packages are ESM-only, you cannot import them directly into your CommonJS Nest app. Instead, you have to use dynamic imports:

```typescript
const adminjsUploadFeature = await import('@adminjs/upload');
```


# Fastify

@adminjs/fastify

{% hint style="info" %}
Make sure you have installed AdminJS packages described in [Getting started](/installation/getting-started) article.

```bash
$ yarn add adminjs @adminjs/fastify
```

{% endhint %}

To setup AdminJS panel with Fastify you need to have `fastify` installed and required peer dependencies:

```bash
$ yarn add fastify tslib
```

Afterwards, follow one of the examples below.

### Simple

{% tabs %}
{% tab title="Javascript" %}
{% code title="app.js" %}

```javascript
import AdminJSFastify from '@adminjs/fastify'
import AdminJS from 'adminjs'
import Fastify from 'fastify'

const PORT = 3000

const start = async () => {
  const app = Fastify()
  const admin = new AdminJS({
    databases: [],
    rootPath: '/admin'
  })

  await AdminJSFastify.buildRouter(
    admin,
    app,
  )
  
  app.listen({ port: PORT }, (err, addr) => {
    if (err) {
      console.error(err)
    } else {
      console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`)
    }
  })
}

start()
```

{% endcode %}
{% endtab %}

{% tab title="Typescript" %}
{% code title="app.ts" %}

```typescript
import AdminJSFastify from '@adminjs/fastify'
import AdminJS from 'adminjs'
import Fastify from 'fastify'

const PORT = 3000

const start = async () => {
  const app = Fastify()
  const admin = new AdminJS({
    databases: [],
    rootPath: '/admin'
  })

  await AdminJSFastify.buildRouter(
    admin,
    app,
  )
  
  app.listen({ port: PORT }, (err, addr) => {
    if (err) {
      console.error(err)
    } else {
      console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`)
    }
  })
}

start()
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Authentication

To add authentication, you must use `AdminJSFastify.buildAuthenticatedRouter` instead of `AdminJSFastify.buildRouter`. Additionally, we must set up a session store to keep our session information. In the example below we will store our session in a Postgres table, we will also use `connect-pg-simple` to allow our session store to connect to the database.

{% tabs %}
{% tab title="Javascript" %}
Install additional dependencies:

```bash
$ yarn add @fastify/session connect-pg-simple
```

{% code title="app.js" %}

```javascript
import AdminJSFastify from '@adminjs/fastify'
import FastifySession from '@fastify/session'
import AdminJS from 'adminjs'
import Fastify from 'fastify'
import Connect from 'connect-pg-simple'

const ConnectSession = Connect(FastifySession)
const sessionStore = new ConnectSession({
  conObject: {
    connectionString: 'postgres://adminjs:adminjs@localhost:5435/adminjs',
    ssl: process.env.NODE_ENV === 'production',
  },
  tableName: 'session',
  createTableIfMissing: true,
})

const PORT = 3000

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email, password) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

const start = async () => {
  const app = Fastify()
  const admin = new AdminJS({
    databases: [],
    rootPath: '/admin'
  })
  
  // "secret" must be a string with at least 32 characters, example:
  const cookieSecret = 'sieL67H7GbkzJ4XCoH0IHcmO1hGBSiG5'
  await AdminJSFastify.buildAuthenticatedRouter(
    admin,
    {
      authenticate,
      cookiePassword: cookieSecret,
      cookieName: 'adminjs',
    },
    app,
    {
      store: sessionStore as any,
      saveUninitialized: true,
      secret: cookieSecret,
      cookie: {
        httpOnly: process.env.NODE_ENV === 'production',
        secure: process.env.NODE_ENV === 'production',
      },
    }
  )
  
  app.listen({ port: PORT }, (err, addr) => {
    if (err) {
      console.error(err)
    } else {
      console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`)

    }
  })
}

start()
```

{% endcode %}

As you may have noticed, the `authenticate` function compares credentials you submit in the form with a hardcoded `DEFAULT_ADMIN` object. In your case, you might want to modify `authenticate` function's logic to compare form credentials against real database objects.

{% hint style="info" %}
If you plan to copy-paste this example, make sure you set the database connection string to a functional one.
{% endhint %}
{% endtab %}

{% tab title="Typescript" %}
Install additional dependencies:

```bash
$ yarn add @fastify/session connect-pg-simple
$ yarn add -D @types/connect-pg-simple
```

{% code title="app.ts" %}

```typescript
import AdminJSFastify from '@adminjs/fastify'
import FastifySession from '@fastify/session'
import AdminJS from 'adminjs'
import Fastify from 'fastify'
import Connect from 'connect-pg-simple'

// Note: There are typing issues between Fastify Session and connect-pg-simple
// but the session will work correctly.
const ConnectSession = Connect(FastifySession as any)
const sessionStore = new ConnectSession({
  conObject: {
    connectionString: 'postgres://adminjs:adminjs@localhost:5435/adminjs',
    ssl: process.env.NODE_ENV === 'production',
  },
  tableName: 'session',
  createTableIfMissing: true,
})

const PORT = 3000

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

const start = async () => {
  const app = Fastify()
  const admin = new AdminJS({
    databases: [],
    rootPath: '/admin'
  })
  
  // "secret" must be a string with at least 32 characters, example:
  const cookieSecret = 'sieL67H7GbkzJ4XCoH0IHcmO1hGBSiG5'
  await AdminJSFastify.buildAuthenticatedRouter(
    admin,
    {
      authenticate,
      cookiePassword: cookieSecret,
      cookieName: 'adminjs',
    },
    app,
    {
      store: sessionStore as any,
      saveUninitialized: true,
      secret: cookieSecret,
      cookie: {
        httpOnly: process.env.NODE_ENV === 'production',
        secure: process.env.NODE_ENV === 'production',
      },
    }
  )
  
  app.listen({ port: PORT }, (err, addr) => {
    if (err) {
      console.error(err)
    } else {
      console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`)

    }
  })
}

start()
```

{% endcode %}

As you may have noticed, the `authenticate` function compares credentials you submit in the form with a hardcoded `DEFAULT_ADMIN` object. In your case, you might want to modify `authenticate` function's logic to compare form credentials against real database objects.

{% hint style="info" %}
If you plan to copy-paste this example, make sure you set the database connection string to a functional one.
{% endhint %}
{% endtab %}
{% endtabs %}


# Hapi

@adminjs/hapi

{% hint style="info" %}
Make sure you have installed AdminJS packages described in [Getting started](/installation/getting-started) article.

```bash
$ yarn add adminjs @adminjs/hapi
```

{% endhint %}

To setup AdminJS panel with Hapi you need to have `@hapi/hapi` installed and required peer dependencies:

```bash
$ yarn add @hapi/hapi @hapi/boom @hapi/cookie @hapi/inert
```

Afterwards, follow one of the examples below.

### Simple

{% tabs %}
{% tab title="Javascript" %}
{% code title="app.js" %}

```javascript
import AdminJSHapi from '@adminjs/hapi'
import Hapi from '@hapi/hapi'

const PORT = 3000

const start = async () => {
  const server = Hapi.server({ port: PORT })

  const adminOptions: ExtendedAdminJSOptions = {
    resources: [],
    rootPath: '/admin',
    auth: {
      isSecure: process.env.NODE_ENV === 'production',
    },
    registerInert: true,
  }

  await server.register({
    plugin: AdminJSHapi,
    options: adminOptions,
  })

  await server.start();
  console.log(`AdminJS available at ${server.info.uri}${adminOptions.rootPath}`);
}

start()
```

{% endcode %}
{% endtab %}

{% tab title="Typescript" %}
{% code title="app.ts" %}

```typescript
import AdminJSHapi, { ExtendedAdminJSOptions } from '@adminjs/hapi'
import Hapi from '@hapi/hapi'

const PORT = 3000

const start = async () => {
  const server = Hapi.server({ port: PORT })

  const adminOptions: ExtendedAdminJSOptions = {
    resources: [],
    rootPath: '/admin',
    auth: {
      isSecure: process.env.NODE_ENV === 'production',
    },
    registerInert: true,
  }

  await server.register<ExtendedAdminJSOptions>({
    plugin: AdminJSHapi,
    options: adminOptions,
  })

  await server.start();
  console.log(`AdminJS available at ${server.info.uri}${adminOptions.rootPath}`);
}

start()
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Authentication

To add authentication, all you have to do is extend `auth` config with `authenticate`, `cookieName` and `cookiePassword`.

{% tabs %}
{% tab title="Javascript" %}
{% code title="app.js" %}

```javascript
import AdminJSHapi from '@adminjs/hapi'
import Hapi from '@hapi/hapi'

const PORT = 3000

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email, password) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

const start = async () => {
  const server = Hapi.server({ port: PORT })

  // "secret" must be a string with at least 32 characters, example:
  const cookieSecret = 'sieL67H7GbkzJ4XCoH0IHcmO1hGBSiG5'
  const adminOptions = {
    resources: [],
    rootPath: '/admin',
    auth: {
      isSecure: process.env.NODE_ENV === 'production',
      authenticate,
      cookieName: 'adminjs',
      cookiePassword: cookieSecret,
    },
    registerInert: true,
  }

  await server.register({
    plugin: AdminJSHapi,
    options: adminOptions,
  })

  await server.start();
  console.log(`AdminJS available at ${server.info.uri}${adminOptions.rootPath}`);
}

start()
```

{% endcode %}
{% endtab %}

{% tab title="Typescript" %}
{% code title="app.ts" %}

```typescript
import AdminJSHapi, { ExtendedAdminJSOptions } from '@adminjs/hapi'
import Hapi from '@hapi/hapi'

const PORT = 3000

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string): Promise<Record<string, unknown> | null> => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

const start = async () => {
  const server = Hapi.server({ port: PORT })

  // "secret" must be a string with at least 32 characters, example:
  const cookieSecret = 'sieL67H7GbkzJ4XCoH0IHcmO1hGBSiG5'
  const adminOptions: ExtendedAdminJSOptions = {
    resources: [],
    rootPath: '/admin',
    auth: {
      isSecure: process.env.NODE_ENV === 'production',
      authenticate,
      cookieName: 'adminjs',
      cookiePassword: cookieSecret,
    },
    registerInert: true,
  }

  await server.register<ExtendedAdminJSOptions>({
    plugin: AdminJSHapi,
    options: adminOptions,
  })

  await server.start();
  console.log(`AdminJS available at ${server.info.uri}${adminOptions.rootPath}`);
}

start()
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Koa

@adminjs/koa

{% hint style="info" %}
Make sure you have installed AdminJS packages described in [Getting started](/installation/getting-started) article.

```bash
$ yarn add adminjs @adminjs/koa
```

{% endhint %}

To setup AdminJS panel with Koa you need to have `koa` installed and required peer dependencies:

```bash
$ yarn add koa @koa/router koa2-formidable
```

Afterwards, follow one of the examples below.

### Simple

{% tabs %}
{% tab title="Javascript" %}
{% code title="app.js" %}

```javascript
import AdminJS from 'adminjs'
import AdminJSKoa from '@adminjs/koa'
import Koa from 'koa'

const PORT = 3000

const start = async () => {
  const app = new Koa()
  const admin = new AdminJS({
    resources: [],
    rootPath: '/admin',
  })

  const router = AdminJSKoa.buildRouter(admin, app)

  app
    .use(router.routes())
    .use(router.allowedMethods())

   app.listen(PORT, () => {
     console.log(`AdminJS available at http://localhost:${PORT}${admin.options.rootPath}`)
   })
}

start()
```

{% endcode %}
{% endtab %}

{% tab title="Typescript" %}
Install additional dependencies:

```bash
$ yarn add -D @types/koa
```

{% code title="app.ts" %}

```typescript
import AdminJS from 'adminjs'
import AdminJSKoa from '@adminjs/koa'
import Koa from 'koa'

const PORT = 3000

const start = async () => {
  const app = new Koa()
  const admin = new AdminJS({
    resources: [],
    rootPath: '/admin',
  })

  const router = AdminJSKoa.buildRouter(admin, app)

  app
    .use(router.routes())
    .use(router.allowedMethods())

   app.listen(PORT, () => {
     console.log(`AdminJS available at http://localhost:${PORT}${admin.options.rootPath}`)
   })
}

start()
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Authenticated

To add authentication, you must use `AdminJSKoa.buildAuthenticatedRouter` instead of `AdminJSKoa.buildRouter`.

{% tabs %}
{% tab title="Javascript" %}
{% code title="app.js" %}

```javascript
import AdminJS from 'adminjs'
import AdminJSKoa from '@adminjs/koa'
import Koa from 'koa'

const PORT = 3000

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email, password) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

const start = async () => {
  const app = new Koa()

  const admin = new AdminJS({
    resources: [],
    rootPath: '/admin',
  })

  app.keys = ['your secret for koa cookie'];
  const router = AdminJSKoa.buildAuthenticatedRouter(
    admin,
    app,
    {
      authenticate,
      sessionOptions: {
        // You may configure your Koa session here
        httpOnly: process.env.NODE_ENV === 'production',
        secure: process.env.NODE_ENV === 'production',
        renew: true,
      },
    },
  )

  app
    .use(router.routes())
    .use(router.allowedMethods())

   app.listen(PORT, () => {
     console.log(`AdminJS available at http://localhost:${PORT}${admin.options.rootPath}`)
   })
}

start()
```

{% endcode %}

As you may have noticed, the `authenticate` function compares credentials you submit in the form with a hardcoded `DEFAULT_ADMIN` object. In your case, you might want to modify `authenticate` function's logic to compare form credentials against real database objects.
{% endtab %}

{% tab title="Typescript" %}
Install additional dependencies:

```bash
$ yarn add -D @types/koa
```

{% code title="app.ts" %}

```typescript
import AdminJS from 'adminjs'
import AdminJSKoa from '@adminjs/koa'
import Koa from 'koa'

const PORT = 3000

const DEFAULT_ADMIN = {
  email: 'admin@example.com',
  password: 'password',
}

const authenticate = async (email: string, password: string) => {
  if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
    return Promise.resolve(DEFAULT_ADMIN)
  }
  return null
}

const start = async () => {
  const app = new Koa()

  const admin = new AdminJS({
    resources: [],
    rootPath: '/admin',
  })

  app.keys = ['your secret for koa cookie'];
  const router = AdminJSKoa.buildAuthenticatedRouter(
    admin,
    app,
    {
      authenticate,
      sessionOptions: {
        // You may configure your Koa session here
        httpOnly: process.env.NODE_ENV === 'production',
        secure: process.env.NODE_ENV === 'production',
        renew: true,
      },
    },
  )

  app
    .use(router.routes())
    .use(router.allowedMethods())

   app.listen(PORT, () => {
     console.log(`AdminJS available at http://localhost:${PORT}${admin.options.rootPath}`)
   })
}

start()
```

{% endcode %}

As you may have noticed, the `authenticate` function compares credentials you submit in the form with a hardcoded `DEFAULT_ADMIN` object. In your case, you might want to modify `authenticate` function's logic to compare form credentials against real database objects.
{% endtab %}
{% endtabs %}


# Community Plugins

While the AdminJS team develops and actively maintains multiple Plugins, there are also ones developed and maintained by our renowned community members.&#x20;


# Matrix

This plugin integrates AdminJS with Matrix, enabling users to log in to the AdminJS panel using Matrix accounts through the MatrixAuthProvider.

To ensure that only authorized personnel can manage administrative functions, users must have administrative privileges in Matrix to authenticate and access AdminJS Matrix features.

[➡️  Authentication - Matrix User Authentication](/basics/authentication/matrixauthprovider)

***

<div><figure><img src="/files/xUcgYdXHsGrGOoXY2EyF" alt=""><figcaption></figcaption></figure> <figure><img src="/files/huEkmzGdm6CfamOebXNt" alt=""><figcaption></figcaption></figure></div>

<figure><img src="/files/zKLzbEGrYNHzRKei5zPs" alt=""><figcaption></figcaption></figure>

### Features

* Seamless integration between AdminJS and Matrix.
* Authentication via Matrix user accounts.
* Resource management for Matrix users, rooms, and devices.

### Installation

Install the package using npm:

```bash
npm install @adminjs/matrix
```

### Environment Variables

Configure the following environment variables, preferably in a `.env` file:

```env
MATRIX_BASE_URL=http://localhost:8001
MATRIX_SERVER_DOMAIN=hhelocal.com
```

### Setup

#### 1. Import `reflect-metadata`

Import at the very beginning of your application’s entry file (e.g., `app.ts`):

```typescript
import 'reflect-metadata';
import express from 'express';
```

#### 2. Create Matrix Resources

Create resource files in `src/resources/matrix/`.

Example of general resource configuration:

```typescript
{
  sort: { sortBy: 'createdAt', direction: 'desc' },
  navigation: { name: 'Matrix', icon: 'MessageSquare' },
  actions: {
    new: { before: [] },
  },
  editProperties: ['name', 'password', 'displayname', 'userType', 'emailAddress', 'isAdmin', 'avatarUrl'],
  properties: {
    emailAddress: {
      type: 'string',
      isVisible: { list: false, filter: true, show: true, edit: true },
    },
  },
  translations: {
    en: {
      actions: { list: 'Matrix Users' },
      properties: {
        threepids: 'Addresses',
        'threepids.medium': 'Type',
      },
    },
  },
}
```

**matrix-device.resource.ts**

```typescript
import { DeviceAdapter, MatrixResourceEnum } from '@adminjs/matrix';

export const createMatrixDeviceResource = DeviceAdapter.buildMatrixDeviceResource({
  resourceType: MatrixResourceEnum.MATRIX_DEVICE,
  baseUrl: process.env.MATRIX_BASE_URL,
  serverDomain: process.env.MATRIX_SERVER_DOMAIN,
  eraseOnDelete: false,
});
```

**matrix-room.resource.ts**

```typescript
import { MatrixResourceEnum, RoomAdapter } from '@adminjs/matrix';

export const createMatrixRoomResource = RoomAdapter.buildMatrixRoomResource({
  resourceType: MatrixResourceEnum.MATRIX_ROOM,
  baseUrl: process.env.MATRIX_BASE_URL,
  serverDomain: process.env.MATRIX_SERVER_DOMAIN,
});
```

**matrix-user.resource.ts**

```typescript
import { MatrixResourceEnum, UserAdapter } from '@adminjs/matrix';

export const createMatrixUserResource = UserAdapter.buildMatrixUserResource({
  resourceType: MatrixResourceEnum.MATRIX_USER,
  baseUrl: process.env.MATRIX_BASE_URL,
  serverDomain: process.env.MATRIX_SERVER_DOMAIN,
  eraseOnDelete: false,
});
```

#### 3. Register Resources in AdminJS

Example `src/admin/options.ts`:

```typescript
import AdminJS, { AdminJSOptions } from 'adminjs';
import { DeviceAdapter, UserAdapter, RoomAdapter, matrixAuthEnTranslations } from '@adminjs/matrix'; 
import { createMatrixUserResource } from '../resources/matrix/matrix-user.resource.js';
import { createMatrixRoomResource } from '../resources/matrix/matrix-room.resource.js';
import { createMatrixDeviceResource } from '../resources/matrix/matrix-device.resource.js';
import componentLoader from './component-loader.js';

const adapters = [
  { Resource: UserAdapter.MatrixUserResource, Database: UserAdapter.MatrixUserDatabase },
  { Resource: RoomAdapter.MatrixRoomResource, Database: RoomAdapter.MatrixRoomDatabase },
  { Resource: DeviceAdapter.MatrixDeviceResource, Database: DeviceAdapter.MatrixDeviceDatabase },
];

adapters.forEach((adapter) => AdminJS.registerAdapter(adapter));

const options: AdminJSOptions = {
  componentLoader,
  rootPath: '/admin',
  resources: [
    createMatrixUserResource,
    createMatrixRoomResource,
    createMatrixDeviceResource,
  ],
  locale: {
    language: 'en',
    translations: {
      en: {
        properties: {
          ...matrixAuthEnTranslations.properties,
        },
        messages: {
          ...matrixAuthEnTranslations.messages,
        },
      },
    },
  },
};

export default options;
```

#### 4. Register Components

In `src/admin/component-loader.ts`:

```typescript
import { bundleMatrixAdapterComponents } from './addons/matrix/adapters/index.js';

bundleMatrixAdapterComponents(componentLoader);
```

### Authentication Methods <a href="#authentication-methods" id="authentication-methods"></a>

There are three ways to handle authentication with this plugin:

#### Method 1: Token-based Authentication <a href="#method-1-token-based-authentication" id="method-1-token-based-authentication"></a>

Add the Matrix token to your authentication provider (`src/admin/auth-provider.ts`):

```typescript
import { DefaultAuthProvider } from 'adminjs';
import componentLoader from './component-loader.js';
import { DEFAULT_ADMIN } from './constants.js';

const provider = new DefaultAuthProvider({
  componentLoader,
  authenticate: async ({ email }) => {
    if (email === DEFAULT_ADMIN.email) {
      return {
        email,
        _auth: {
          matrixAccessToken: process.env.MATRIX_ACCESS_TOKEN,
        },
      };
    }
    return null;
  },
});

export default provider;
```

Obtain your Matrix token by logging into Matrix. Execute this command on a machine that has access to the Matrix server:

```bash
curl --location 'http://localhost:8001/_matrix/client/r0/login' --header 'Content-Type: application/json' --data-raw '{ "type": "m.login.password", "user": "your_username", "password": "your_password" }'
```

Ensure the Matrix user is already registered and has administrative privileges. This command should be executed directly on the Matrix server as it's a built-in Matrix-Synapse tool:

```bash
register_new_matrix_user -c /path/to/matrix-synapse/homeserver.yaml http://localhost:8008
```

Note: The path to the homeserver.yaml file may vary depending on your Matrix installation. Common locations include:

/etc/matrix-synapse/homeserver.yaml /data/homeserver.yaml (if using Docker) Note: This method uses a single shared token for all AdminJS users, which remains valid indefinitely unless manually revoked.

#### Method 2: Matrix User Authentication <a href="#method-2-matrix-user-authentication" id="method-2-matrix-user-authentication"></a>

[Authentication - Matrix User Authentication](/basics/authentication/matrixauthprovider)<br>

* Using this authentication method, ensure that the user's account has administrative privileges in Matrix.

#### Method 3: Custom Authentication Provider <a href="#method-3-custom-authentication-provider" id="method-3-custom-authentication-provider"></a>

You can also create a custom provider and manually handle tokens. Here's an example implementation of a custom authentication provider based on email and password:

```typescript
import { DefaultAuthProvider } from 'adminjs';
import componentLoader from './component-loader.js';

const provider = new DefaultAuthProvider({
  componentLoader,
  authenticate: async ({ email, password }) => {
    // Example implementation - replace with your own authentication logic
    const user = await db.users.findOne({ email });

    if (user && await verifyPassword(user.password, password)) {
      // Get or generate a Matrix token for this user
      const matrixToken = await getMatrixTokenForUser(user);

      return {
        email: user.email,
        _auth: {
          matrixAccessToken: matrixToken
        }
      };
    }

    return null;
  }
});

export default provider;
```

\
Contributing

Contributions to the `@adminjs/matrix` plugin are welcome. Please submit pull requests or open issues on GitHub for bug fixes, improvements, or new features.

### License

This plugin is licensed under the MIT License.


# Adapters

This section contains detailed instructions on how to connect your AdminJS instance to a database using one of the adapters listed below.

{% content-ref url="/pages/KK6VyB7yHuoR7azW2pF8" %}
[TypeORM](/installation/adapters/typeorm)
{% endcontent-ref %}

{% content-ref url="/pages/ES5iKXTjdtNmPD6kevQd" %}
[Sequelize](/installation/adapters/sequelize)
{% endcontent-ref %}

{% content-ref url="/pages/8bIO043UBxSBApxhO32v" %}
[Mongoose](/installation/adapters/mongoose)
{% endcontent-ref %}

{% content-ref url="/pages/3zQiRTEexQy7LjnbCXdE" %}
[Prisma](/installation/adapters/prisma)
{% endcontent-ref %}

{% content-ref url="/pages/cUqoB73WJUkM6GpBEn6j" %}
[MikroORM](/installation/adapters/mikroorm)
{% endcontent-ref %}

{% content-ref url="/pages/BU1gl2QAN1g4rMhFSY3R" %}
[Objection](/installation/adapters/objection)
{% endcontent-ref %}

{% content-ref url="/pages/2cAqScHFY4HhPLmooPdG" %}
[SQL](/installation/adapters/sql)
{% endcontent-ref %}

{% content-ref url="/pages/kbYgp2yoznunYayz4cdY" %}
[Community Adapters](/installation/adapters/community-adapters)
{% endcontent-ref %}


# TypeORM

@adminjs/typeorm

{% hint style="info" %}
Before reading this article, make sure you have set up an AdminJS instance using one of the supported [Plugins](/installation/plugins).\
Additionally, you should have installed `@adminjs/typeorm` as described in [Getting started](/installation/getting-started) section.
{% endhint %}

This guide will assume you have set up TypeORM using it's [documentation](https://typeorm.io/#quick-start) or [Nest.js documentation](https://docs.nestjs.com/recipes/sql-typeorm).

`@adminjs/typeorm` currently only supports entities that extend `BaseEntity` as shown on the example below:

{% code title="organization.entity.ts" %}

```typescript
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'

@Entity({ name: 'organizations' })
export class Organization extends BaseEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column()
  public name: string;
}
```

{% endcode %}

There are small differences in how you connect TypeORM to Nest.js vs other plugins, so the guide will be split into two sections accordingly.

### Standard

Make sure you have followed the tutorial for the framework you are using in the [Plugins](/installation/plugins) section.

The configuration for non-Nest.js plugins is basically the same for each one of them:

* You must import the data source and initialize it
* You must import `AdminJSTypeorm` adapter and register it
* You must import the entities you want to use and pass them to AdminJS `resources` options

{% code title="app.ts" %}

```typescript
// ... other imports
import * as AdminJSTypeorm from '@adminjs/typeorm'

import dataSource from './path/to/your/datasource.js'
import { Organization } from './organization.entity.js'

AdminJS.registerAdapter({
  Resource: AdminJSTypeorm.Resource,
  Database: AdminJSTypeorm.Database,
})

// ... other code
const start = async () => {
  // Make sure you initialize the data source before you create your AdminJS instance
  await dataSource.initialize()
  const adminOptions = {
    // We pass Organization to `resources`
    resources: [Organization],
  }
  // Please note that some plugins don't need you to create AdminJS instance manually,
  // instead you would just pass `adminOptions` into the plugin directly,
  // an example would be "@adminjs/hapi"
  const admin = new AdminJS(adminOptions)
  // ... other code
}

start()
```

{% endcode %}

### Nest.js

Make sure you have set up your `app.module.ts` according to [Nest.js documentation](https://docs.nestjs.com/recipes/sql-typeorm) and you have followed [Nest.js plugin tutorial ](/installation/plugins/nest)as well.

Your `app.module.ts` should have `imports` option which contains:

* `TypeOrmModule.forRoot(params)` to set up TypeORM
* `AdminModule.createAdminAsync({ ... }`

In your `app.module.ts` add these imports at the top of the file:

{% code title="app.module.ts" %}

```typescript
import * as AdminJSTypeorm from '@adminjs/typeorm'
import AdminJS from 'adminjs'
```

{% endcode %}

Following this, register `AdminJSTypeorm` adapter somewhere after your imports:

{% code title="app.module.ts" %}

```typescript
AdminJS.registerAdapter({
  Resource: AdminJSTypeorm.Resource,
  Database: AdminJSTypeorm.Database,
})
```

{% endcode %}

This will allow you to pass TypeORM models for AdminJS to load. If we use the `Organization` entity that we used as en example earlier, you should import it into `app.module.ts` and pass it into `resources` in your `adminJsOptions`:

{% code title="app.module.ts" %}

```typescript
// ... other imports
import { Organization } from './organization.entity.js'
// ... other code
AdminModule.createAdminAsync({
  useFactory: () => ({
    adminJsOptions: {
      rootPath: '/admin',
      resources: [Organization],
    },
  }),
}),
// ... other code
```

{% endcode %}


# Sequelize

@adminjs/sequelize

{% hint style="info" %}
Before reading this article, make sure you have set up an AdminJS instance using one of the supported [Plugins](/installation/plugins).\
Additionally, you should have installed `@adminjs/sequelize` as described in [Getting started](/installation/getting-started) section.
{% endhint %}

This guide will assume you have set up Sequelize using it's [documentation](https://sequelize.org/docs/v6/getting-started/) or [Nest.js documentation](https://docs.nestjs.com/recipes/sql-sequelize).

There are small differences in how you connect Sequelize to Nest.js vs other plugins, so the guide will be split into two sections accordingly.

Example model:

{% code title="category.entity.ts" %}

```typescript
import { DataTypes, Model, Optional } from 'sequelize'

import sequelize from './index.js'

interface ICategory {
  id: number;
  name: string;
  createdAt: Date;
  updatedAt: Date;
}

export type CategoryCreationAttributes = Optional<ICategory, 'id'>

export class Category extends Model<ICategory, CategoryCreationAttributes> {
  declare id: number;
  declare name: string;
  declare createdAt: Date;
  declare updatedAt: Date;
}

Category.init(
  {
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true,
    },
    name: {
      type: new DataTypes.STRING(128),
      allowNull: false,
    },
    createdAt: {
      type: DataTypes.DATE,
    },
    updatedAt: {
      type: DataTypes.DATE,
    },
  },
  {
    sequelize,
    tableName: 'categories',
    modelName: 'category',
  }
)
```

{% endcode %}

Sequelize connection:

```typescript
import { Sequelize } from 'sequelize'

const sequelize = new Sequelize('postgres://adminjs:adminjs@localhost:5435/adminjs', {
  dialect: 'postgres',
})

export default sequelize
```

### Standard

Make sure you have followed the tutorial for the framework you are using in the [Plugins](/installation/plugins) section.

The configuration for non-Nest.js plugins is basically the same for each one of them:

* You must import `AdminJSSequelize` adapter and register it
* You must import the entities you want to use and pass them to AdminJS `resources` options

{% code title="app.ts" %}

```typescript
// ... other imports
import * as AdminJSSequelize from '@adminjs/sequelize'

import { Category } from './category.entity.js'

AdminJS.registerAdapter({
  Resource: AdminJSSequelize.Resource,
  Database: AdminJSSequelize.Database,
})

// ... other code
const start = async () => {
  const adminOptions = {
    // We pass Category to `resources`
    resources: [Category],
  }
  // Please note that some plugins don't need you to create AdminJS instance manually,
  // instead you would just pass `adminOptions` into the plugin directly,
  // an example would be "@adminjs/hapi"
  const admin = new AdminJS(adminOptions)
  // ... other code
}

start()
```

{% endcode %}

### Nest.js

Make sure you have set up your `app.module.ts` according to [Nest.js documentation](https://docs.nestjs.com/recipes/sql-typeorm) and you have followed [Nest.js plugin tutorial ](/installation/plugins/nest)as well.

Your `app.module.ts` should have `imports` option which contains:

* `SequelizeModule.forRoot({ uri: '...', dialect: '...' })` to set up Sequelize
* `AdminModule.createAdminAsync({ ... }`

{% hint style="info" %}
You should also be able to get Sequelize to work if you set it up using a Nest.js database provider. The main point is to have a database connection established before AdminJS is initialized.
{% endhint %}

In your `app.module.ts` add these imports at the top of the file:

{% code title="app.module.ts" %}

```typescript
import * as AdminJSSequelize from '@adminjs/sequelize'
import AdminJS from 'adminjs'
```

{% endcode %}

Following this, register `AdminJSSequelize` adapter somewhere after your imports:

{% code title="app.module.ts" %}

```typescript
AdminJS.registerAdapter({
  Resource: AdminJSSequelize.Resource,
  Database: AdminJSSequelize.Database,
})
```

{% endcode %}

This will allow you to pass Sequelize models for AdminJS to load. If we use the `Category` entity that we used as en example earlier, you should import it into `app.module.ts` and pass it into `resources` in your `adminJsOptions`:

{% code title="app.module.ts" %}

```typescript
// ... other imports
import { Category } from './category.entity.js'
// ... other code
AdminModule.createAdminAsync({
  useFactory: () => ({
    adminJsOptions: {
      rootPath: '/admin',
      resources: [Category],
    },
  }),
}),
// ... other code
```

{% endcode %}


# Prisma

@adminjs/prisma

{% hint style="info" %}
Before reading this article, make sure you have set up an AdminJS instance using one of the supported [Plugins](/installation/plugins).\
Additionally, you should have installed `@adminjs/prisma` as described in [Getting started](/installation/getting-started) section.
{% endhint %}

This guide will assume you have set up Prisma using it's [documentation](https://www.prisma.io/docs/getting-started/quickstart) or [Nest.js documentation](https://docs.nestjs.com/recipes/prisma).

There are small differences in how you connect Prisma to Nest.js vs other plugins, so the guide will be split into two sections accordingly.

Example Prisma schema:

{% code title="prisma.schema" %}

```typescript
datasource db {
  provider = "mysql"
  url      = env("MYSQL_DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Publisher {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  name    String?
}

```

{% endcode %}

### Standard

Make sure you have followed the tutorial for the framework you are using in the [Plugins](/installation/plugins) section.

The configuration for non-Nest.js plugins is basically the same for each one of them:

* You must instantiate a Prisma Client before creating `AdminJS` instance
* You must import `AdminJSPrisma` adapter and register it
* You must import the entities you want to use and pass them to AdminJS `resources` options

{% code title="app.ts" %}

```typescript
// ... other imports
import { Database, Resource, getModelByName } from '@adminjs/prisma'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

AdminJS.registerAdapter({ Database, Resource })

// ... other code
const start = async () => {
  const adminOptions = {
    resources: [{
      resource: { model: getModelByName('Post'), client: prisma },
      options: {},
    }, {
      resource: { model: getModelByName('Profile'), client: prisma },
      options: {},
    }, {
      resource: { model: getModelByName('Publisher'), client: prisma },
      options: {},
    }],
  }
  // Please note that some plugins don't need you to create AdminJS instance manually,
  // instead you would just pass `adminOptions` into the plugin directly,
  // an example would be "@adminjs/hapi"
  const admin = new AdminJS(adminOptions)
  // ... other code
}

start()
```

{% endcode %}

### Nest.js

Make sure you have set up your `app.module.ts` according to [Nest.js documentation](https://docs.nestjs.com/recipes/prisma) and you have followed [Nest.js plugin tutorial ](/installation/plugins/nest)as well.

Your `app.module.ts` should have `imports` option which contains:

* `AdminModule.createAdminAsync({ ... }`

In your `app.module.ts` add these imports at the top of the file:

{% code title="app.module.ts" %}

```typescript
import { Database, Resource, getModelByName } from '@adminjs/prisma'
import AdminJS from 'adminjs'

import { PrismaService } from './prisma.service.js' // PrismaService from Nest.js documentation
```

{% endcode %}

Following this, register `AdminJSPrisma` adapter somewhere after your imports:

{% code title="app.module.ts" %}

```typescript
AdminJS.registerAdapter({ Database, Resource })
```

{% endcode %}

This will allow you to pass Prisma models for AdminJS to load. If we use the `Publisher` entity that we used as en example earlier, you should import it into `app.module.ts` and pass it into `resources` in your `adminJsOptions`:

{% code title="app.module.ts" %}

```typescript
// ... other imports
import { Category } from './category.entity.js'
// ... other code
AdminModule.createAdminAsync({
  useFactory: () => {
    // Note: Feel free to contribute to this documentation if you find a Nest-way of
    // injecting PrismaService into AdminJS module
    const prisma = new PrismaService()

    return {
      adminJsOptions: {
        rootPath: '/admin',
        resources: [{
          resource: { model: getModelByName('Post'), client: prisma },
          options: {},
        }],
      },
    }
  }
}),
// ... other code
```

{% endcode %}

### Custom client module

In case your generated client is not under the default path, you can pass `clientModule` to each resource's configuration:

```typescript
// other imports
// your custom prisma module
import PrismaModule from '../prisma/client-prisma/index.js';

// ...

const prisma = new PrismaModule.PrismaClient();

// ...

// Notice `clientModule` per resource
const admin = new AdminJS({
  resources: [{
    resource: {
      model: getModelByName('Post', PrismaModule),
      client: prisma,
      clientModule: PrismaModule,
    },
  }],
});
```

<br>


# MikroORM

@adminjs/mikroorm

{% hint style="info" %}
Before reading this article, make sure you have set up an AdminJS instance using one of the supported [Plugins](/installation/plugins).\
Additionally, you should have installed `@adminjs/mikroorm` as described in [Getting started](/installation/getting-started) section.
{% endhint %}

This guide will assume you have set up MikroORM using it's [documentation](https://mikro-orm.io/docs/installation) or [Nest.js documentation](https://docs.nestjs.com/recipes/mikroorm).

There are small differences in how you connect MikroORM to Nest.js vs other plugins, so the guide will be split into two sections accordingly.

Example model:

{% code title="owner.entity.ts" %}

```typescript
import { v4 } from 'uuid'
import { BaseEntity, Entity, PrimaryKey, Property } from '@mikro-orm/core'

export interface IOwner {
  firstName: string;
  lastName: string;
  age: number;
}

@Entity({ tableName: 'owners' })
export class Owner extends BaseEntity<Owner, 'id'> implements IOwner {
  @PrimaryKey({ columnType: 'uuid' })
  id = v4()

  @Property({ fieldName: 'first_name', columnType: 'text' })
  firstName: string

  @Property({ fieldName: 'last_name', columnType: 'text' })
  lastName: string

  @Property({ fieldName: 'age', columnType: 'integer' })
  age: number

  @Property({ fieldName: 'created_at', columnType: 'timestamptz' })
  createdAt: Date = new Date()

  @Property({
    fieldName: 'updated_at',
    columnType: 'timestamptz',
    onUpdate: () => new Date(),
  })
  updatedAt: Date = new Date()
}
```

{% endcode %}

### Standard

Make sure you have followed the tutorial for the framework you are using in the [Plugins](/installation/plugins) section.

The configuration for non-Nest.js plugins is basically the same for each one of them:

* You must initialize MikroORM before creating `AdminJS` instance
* You must import `AdminJSMikroORM` adapter and register it
* You must import the entities you want to use and pass them to AdminJS `resources` options

{% code title="app.ts" %}

```typescript
// ... other imports
import { MikroORM } from '@mikro-orm/core'
import * as AdminJSMikroORM from '@adminjs/mikroorm'
import { Owner } from './owner.entity.js'

AdminJS.registerAdapter({
  Resource: AdminJSMikroORM.Resource,
  Database: AdminJSMikroORM.Database,
})

  // Note: `config` is your MikroORM configuration as described in it's docs
const config = {
  entities: [Owner],
  dbName: 'adminjs',
  type: 'postgresql',
  clientUrl: 'postgres://adminjs:adminjs@localhost:5435/adminjs',
}

// ... other code
const start = async () => {
  const orm = await MikroORM.init(config)
  const adminOptions = {
    // We pass Owner to `resources`
    resources: [{
      resource: { model: Owner, orm },
      options: {}
    }],
  }
  // Please note that some plugins don't need you to create AdminJS instance manually,
  // instead you would just pass `adminOptions` into the plugin directly,
  // an example would be "@adminjs/hapi"
  const admin = new AdminJS(adminOptions)
  // ... other code
}

start()
```

{% endcode %}

### Nest.js

Make sure you have set up your `app.module.ts` according to [Nest.js documentation](https://docs.nestjs.com/recipes/mikroorm) and you have followed [Nest.js plugin tutorial ](/installation/plugins/nest)as well.

Your `app.module.ts` should have `imports` option which contains:

* `MikroOrmModule.forRoot(...)` to set up MikroORM:

```typescript
// Note: this is a default configuration from Nest.js documentation
MikroOrmModule.forRoot({
  entities: ['./dist/entities'],
  entitiesTs: ['./src/entities'],
  dbName: 'my-db-name.sqlite3',
  type: 'sqlite',
})
```

* `AdminModule.createAdminAsync({ ... }`

In your `app.module.ts` add these imports at the top of the file:

{% code title="app.module.ts" %}

```typescript
import * as AdminJSMikroORM from '@adminjs/mikroorm'
import AdminJS from 'adminjs'
```

{% endcode %}

Following this, register `AdminJSMikroORM` adapter somewhere after your imports:

{% code title="app.module.ts" %}

```typescript
AdminJS.registerAdapter({
  Resource: AdminJSMikroORM.Resource,
  Database: AdminJSMikroORM.Database,
})
```

{% endcode %}

This will allow you to pass MikroORM models for AdminJS to load. If we use the `Owner` entity that we used as en example earlier, you should import it into `app.module.ts` and pass it into `resources` in your `adminJsOptions`:

{% code title="app.module.ts" %}

```typescript
// ... other imports
import { Owner } from './owner.entity.js'
// ... other code
AdminModule.createAdminAsync({
  useFactory: () => ({
    adminJsOptions: {
      rootPath: '/admin',
      resources: [{
        resource: { model: Owner, orm },
        options: {}
      }],
    },
  }),
}),
// ... other code
```

{% endcode %}


# Objection

@adminjs/objection

{% hint style="info" %}
Before reading this article, make sure you have set up an AdminJS instance using one of the supported [Plugins](/installation/plugins).\
Additionally, you should have installed `@adminjs/objection` as described in [Getting started](/installation/getting-started) section.
{% endhint %}

This guide will assume you have set up Objection using it's [documentation](https://vincit.github.io/objection.js/guide/).

Before you start connecting your Objection models to AdminJS, we strongly advise to extend it's `Model` class to include format options and hooks for setting up your timestamps.

You might need to install an additional library `ajv-formats`:

```bash
$ yarn add ajv-formats
```

{% code title="base-model.ts" %}

```typescript
import addFormats from 'ajv-formats';
import { AjvValidator, Model } from 'objection';

export abstract class BaseModel extends Model {
  createdAt: string;

  updatedAt: string;

  static createValidator(): AjvValidator {
    return new AjvValidator({
      onCreateAjv: (ajv) => {
        addFormats(ajv);
      },
      options: {
        allErrors: true,
        validateSchema: false,
        ownProperties: true,
      },
    });
  }

  $beforeInsert(): void {
    this.createdAt = new Date().toISOString();
  }

  $beforeUpdate(): void {
    this.updatedAt = new Date().toISOString();
  }
}

```

{% endcode %}

`Office` is an example model which extends `BaseModel`:

{% code title="office.entity.ts" %}

```typescript
import { BaseModel } from '../base-model.js';
import Manager from './manager.entity.js';

export interface OfficeAddress {
  street: string;
  city: string;
  zipCode: string;
}

class Office extends BaseModel {
  id: number;

  name: string;

  address?: OfficeAddress;

  static tableName = 'offices';

  static jsonSchema = {
    type: 'object',
    required: ['name'],
    properties: {
      id: { type: 'integer' },
      name: { type: 'string', minLength: 1, maxLength: 255 },
      address: {
        type: 'object',
        properties: {
          street: { type: 'string' },
          city: { type: 'string' },
          zipCode: { type: 'string' },
        },
      },
      createdAt: { type: 'string', format: 'date-time' },
      updatedAt: { type: 'string', format: 'date-time' },
    },
  };
}

export default Office;
```

{% endcode %}

Please note that `jsonSchema` is necessary for AdminJS to determine your fields and their types.

The rest of the setup is similar to other adapters:

* You must import `AdminJSObjection` adapter and register it
* You must import the entities you want to use and pass them to AdminJS `resources` options, you cannot use `databases` because unfortunately Objection does not expose any connection with all models metadata.

{% code title="app.ts" %}

```typescript
// ... other imports
import * as AdminJSObjection from '@adminjs/objection'

import { Office } from './office.entity.js'

AdminJS.registerAdapter({
  Resource: AdminJSObjection.Resource,
  Database: AdminJSObjection.Database,
})

// ... other code
const start = async () => {
  const adminOptions = {
    // We pass Office to `resources`
    resources: [Office],
  }
  // Please note that some plugins don't need you to create AdminJS instance manually,
  // instead you would just pass `adminOptions` into the plugin directly,
  // an example would be "@adminjs/hapi"
  const admin = new AdminJS(adminOptions)
  // ... other code
}

start()
```

{% endcode %}

{% hint style="warning" %}
Make sure you have followed Objection documentation and have setup your `knexfile` and `knex` instance.
{% endhint %}

### Nest.js Support

A guide for Nest.js is a work in progress. If you would like to contribute to the documentation, please contact us.


# SQL

@adminjs/sql

{% hint style="info" %}
Before reading this article, make sure you have set up an AdminJS instance using one of the supported [Plugins](/installation/plugins).\
Additionally, you should have installed `@adminjs/sql` as described in [Getting started](/installation/getting-started) section.
{% endhint %}

`@adminjs/sql` is an official adapter for SQL databases which does not require you to use an ORM and allows you to simply connect your admin panel directly to the database.

### Currently supported databases

* PostgreSQL
* more coming soon

## Usage

Most of the setup is very similar to other AdminJS adapters. The main difference is that you must feed your database credentials to the Adapter instead of an ORM so that it can parse your database schema and run queries against your database.

First of all, import `Adapter`, `Resource` and `Database` from `@adminjs/sql` .

```typescript
import { Adapter, Resource, Database } from '@adminjs/sql'
import AdminJS from 'adminjs'
```

Afterwards, register the adapter and initialize it to let it read your database schema:

```typescript
// ...

AdminJS.registerAdapter({
  Database,
  Resource,
})

// ...

const db = await new Adapter('postgresql', {
  connectionString: '<your database url>',
  database: '<your database name>',
}).init()
```

When creating an `Adapter` instance, the first parameter is your database's dialect (currently only `postgresql` can be used) while the second parameter is an object with your database connection options. Please refer to [Knex documentation](https://knexjs.org/guide/#configuration-options) to find out how the configuration object should look like as `@adminjs/sql` is based on Knex.

Finally, create an `AdminJS` instance and either include `db` in `databases` option to load your entire database (not recommended) or define `resources` separately:

```typescript
const admin = new AdminJS({
  resources: [
    {
      resource: db.table('users'),
      options: {},
    },
  ],
  // databases: [db], <- not recommended
});
```

The rest of the configuration is identical to other plugins and adapters. If you are confused, you can use the example below as a reference:

```typescript
import AdminJS from 'adminjs'
import express from 'express'
import Plugin from '@adminjs/express'
import { Adapter, Database, Resource } from '@adminjs/sql'

AdminJS.registerAdapter({
  Database,
  Resource,
})

const start = async () => {
  const app = express()

  const db = await new Adapter('postgresql', {
    connectionString: 'postgres://adminjs:adminjs@localhost:5432/adminjs_panel',
    database: 'adminjs_panel',
  }).init();

  const admin = new AdminJS({
    resources: [
      {
        resource: db.table('users'),
        options: {},
      },
    ],
  });

  admin.watch()

  const router = Plugin.buildRouter(admin)

  app.use(admin.options.rootPath, router)

  app.listen(8080, () => {
    console.log('app started')
  })
}

start()
```

## Database Relations

Currently only `many-to-one` relation works out of the box if you specify foreign key constraints in your database. Other relations will require you to make UI/backend customizations. Please see our [documentation](https://docs.adminjs.co) to learn more.

## Enums

As of version `1.0.0` database enums aren't automatically detected and loaded. You can assign them manually in your resource options:

```typescript
// ...
  const admin = new AdminJS({
    resources: [{
      resource: db.table('users'),
      options: {
        properties: {
          role: {
            availableValues: [
              { label: 'Admin', value: 'ADMIN' },
              { label: 'Client', value: 'CLIENT' },
            ],
          },
        },
      },
    }],
  })
// ...
```

## Timestamps

If your database tables have automatically default-set timestamps (`created_at`, `updated_at`, etc) they will be visible in create/edit forms by default. You can hide them in resource options:

```typescript
// ...
  const admin = new AdminJS({
    resources: [{
      resource: db.table('users'),
      options: {
        properties: {
          created_at: { isVisible: false },
          updated_at: { isVisible: false },
        },
      },
    }],
  })
// ...
```


# Mongoose

@adminjs/mongoose

{% hint style="info" %}
Before reading this article, make sure you have set up an AdminJS instance using one of the supported [Plugins](/installation/plugins).\
Additionally, you should have installed `@adminjs/mongoose` as described in [Getting started](/installation/getting-started) section.
{% endhint %}

This guide will assume you have set up Mongoose using it's [documentation](https://mongoosejs.com/docs/index.html) or [Nest.js documentation](https://docs.nestjs.com/recipes/sql-sequelize).

There are small differences in how you connect Mongoose to Nest.js vs other plugins, so the guide will be split into two sections accordingly.

Example model:

{% code title="category.entity.ts" %}

```typescript
import { model, Schema, Types } from 'mongoose'

export interface ICategory {
  title: string;
}

export const CategorySchema = new Schema<ICategory>(
  {
    title: { type: 'String', required: true },
  },
  { timestamps: true },
)

export const Category = model<ICategory>('Category', CategorySchema);
```

{% endcode %}

### Standard

Make sure you have followed the tutorial for the framework you are using in the [Plugins](/installation/plugins) section.

The configuration for non-Nest.js plugins is basically the same for each one of them:

* You must connect to Mongo before creating `AdminJS` instance
* You must import `AdminJSMongoose` adapter and register it
* You must import the entities you want to use and pass them to AdminJS `resources` options

{% code title="app.ts" %}

```typescript
// ... other imports
import mongoose from 'mongoose'
import * as AdminJSMongoose from '@adminjs/mongoose'

import { Category } from './category.entity.js'

AdminJS.registerAdapter({
  Resource: AdminJSMongoose.Resource,
  Database: AdminJSMongoose.Database,
})

// ... other code
const start = async () => {
  await mongoose.connect('<mongo db url>')
  const adminOptions = {
    // We pass Category to `resources`
    resources: [Category],
  }
  // Please note that some plugins don't need you to create AdminJS instance manually,
  // instead you would just pass `adminOptions` into the plugin directly,
  // an example would be "@adminjs/hapi"
  const admin = new AdminJS(adminOptions)
  // ... other code
}

start()
```

{% endcode %}

### Nest.js

Make sure you have set up your `app.module.ts` according to [Nest.js documentation](https://docs.nestjs.com/techniques/mongodb) and you have followed [Nest.js plugin tutorial ](/installation/plugins/nest)as well.

Your `app.module.ts` should have `imports` option which contains:

* `MongooseModule.forRoot('<mongo db url>')` to set up Mongoose
* `AdminModule.createAdminAsync({ ... }`

In your `app.module.ts` add these imports at the top of the file:

{% code title="app.module.ts" %}

```typescript
import * as AdminJSMongoose from '@adminjs/mongoose'
import AdminJS from 'adminjs'
```

{% endcode %}

Following this, register `AdminJSMongoose` adapter somewhere after your imports:

{% code title="app.module.ts" %}

```typescript
AdminJS.registerAdapter({
  Resource: AdminJSMongoose.Resource,
  Database: AdminJSMongoose.Database,
})
```

{% endcode %}

This will allow you to pass Mongoose models for AdminJS to load. If we use the `Category` entity that we used as en example earlier, you should import it into `app.module.ts` and pass it into `resources` in your `adminJsOptions`:

{% code title="app.module.ts" %}

```typescript
// ... other imports
import { Category } from './category.entity.js'
// ... other code
AdminModule.createAdminAsync({
  useFactory: () => ({
    adminJsOptions: {
      rootPath: '/admin',
      resources: [Category],
    },
  }),
}),
// ... other code
```

{% endcode %}


# Community Adapters

While the AdminJS team developed and actively maintain multiple Adapters, there are also ones developed and maintained by our renowned community members.&#x20;


# What's new in v7?

This sections covers all new features that will be available in version 7 of `adminjs` core library and  compatible `@adminjs/*` packages.

Please make sure to read our [Migration Guide v7](/installation/migration-guide-v7) article to learn which changes are necessary to migrate if you're already using AdminJS.

## AdminJS is now only ESM-only compatible

With version 7 AdminJS has fully moved to ESM and will no longer support CJS projects.

While we have mixed feelings about the actual benefits ESM brings, it's considered to be a future standard. A migration from CJS to ESM itself is a breaking change. Because of this, a major release seemed to be the correct place for this transition.

However, with this transition, we are also abandoning the support for CJS. The reason for this is that dual packaging of CJS/ESM is a complex topic that we'd decided not to delve deeper into. We've had many issues with libraries that support both at the same time during our migration work, some of them are still not completely resolved and we had to use some temporary (hopefully) workarounds.

## The login page is now SPA

Previously, the login page had been server side rendered which limited developers in how they can customize it without overriding the server's endpoints. An example would be that you couldn't use React hooks or custom events in your login page component, being able to only modify the general look of the HTML form.

This has been changed in version 7 of AdminJS by making the login page a SPA plus it can now be overriden similarly to other custom components.

## Design System updates

A lot of design system components have received a minor visual refresh in order to make the default user interface cleaner.

We have also introduced two new design system components which you can use in your projects:

* `Avatar`
* `Tabs`

### Avatar

`Avatar` is a new component which displays a rounded image of your choice and other content (for example the first letter of your name) in the middle of it.

```typescript
import { Avatar } from "@adminjs/design-system"

export const ExampleAvatar = (props) => {
  const { avatarUrl, email } = props

  return (
    <Box>
      <Avatar src={avatarUrl} alt={email}>
        {email.charAt(0)}
      </Avatar>
    </Box>
  )
}
```

### Tabs

`Tabs` is a new component which allows you to group your content into separate tabs.

```typescript
import { Tab, Tabs, Box } from '@adminjs/design-system'
import React, { useState } from 'react'

export const ExampleTabs = () => {
  const [selectedTab, setSelectedTab] = useState('first')

  return (
    <Tabs currentTab={selectedTab} onChange={setSelectedTab}>
      <Tab id="first" label="First tab">
        First
      </Tab>
      <Tab id="second" label="Second tab">
        <Box color="primary100">Second</Box>
      </Tab>
      <Tab id="third" label="Third tab">
        Third
      </Tab>
    </Tabs>
  )
}
```

`@adminjs/design-system` version 4 comes with some breaking changes. Make sure you read our [migration guide](/installation/migration-guide-v7) to help you get past them.

Additionally, you can find new and updated design system components in our [Storybook](https://storybook.adminjs.co/).

## Themes

`@adminjs/themes` introduces new UI customization options for developers.

#### Features

* You can provide a custom configuration (<https://styled-system.com/theme-specification/>) per theme, allowing you to easily modify the general look of your admin panel.
* You can provide a custom `style.css` file per theme if you need to.
* You can override any AdminJS default component per theme. Please note that if you override the same component yourself, your own component will take precedence and will be used across all your themes.
* You can assign different themes to specific users based on their role or whatever logic you wish to use. This is done by setting `theme` in `currentAdmin` object.

## Translations are now client-side only

The localization is now entirely client-side. Please read our [migration guide](/installation/migration-guide-v7) to learn how to adjust your existing translations.

## Language Selector

By providing `availableLanguages` in your `locale` configuration, you now give a choice for users to select a language they want to see the admin panel in.

```
locale: { 
  language: 'pl', // default language
  availableLanguages: ['en', 'pl'], 
}
```

## You can now translate pages in sidebar

Until now, the name of a page had been shared between what's visible in sidebar and what's present in your browser's address bar. In version 7 you are now able to translate your custom page's label in the sidebar.

```typescript
const admin = new AdminJS({
  pages: {
    myCustomPage: { /* */ },
  }
})
```

The example above will result in the page name appearing as `pages/myCustomPage` in the browser's address pathname, but the sidebar will display it as `My Custom Page`. If you'd like to change its label in the sidebar or define a different name per language, you can now do it in your `locale` settings.

```typescript
"pages": {
  "myCustomPage": "Some Custom Page"
}
```

`pages` is a new section you can define in your translations configuration object.

## There is a new section for translating components

Similarly to `pages`, there's a new `components` section in locale configuration which should help you group your translations by scoping them to your custom components. Additionally, this is what will be used from now on to translate messages in default components from `@adminjs/design-system`.

```
components: { 
  DropZone: {
    placeholder: "Спуштете ја вашата датотека овде или кликнете за да пребарувате",
    acceptedSize: "Максимална големина: {{maxSize}}",
    acceptedType: "Поддржува: {{mimeTypes}}",
    unsupportedSize: "Датотека {{fileName}} е преголем",
    unsupportedType: "Датотека {{fileName}} има неподдржан тип: {{fileType}}"
  }
},
```

The example above shows how you can change the messages in `DropZone` component per language. Previously, the messages had been hardcoded in English.

```typescript
import { DropZone } from '@adminjs/design-system'
import { useTranslation } from 'adminjs'

export const ExampleDropZone = () => {
  const { translateComponent } = useTranslation()
  
  return (
     <Box>
       <Label>Attachment</Label>
       <DropZone
         validate={{ maxSize: 102400, mimeTypes: ['application/pdf'] }}
         translations={translateComponent('DropZone', { returnObjects: true })}
       />  
    </Box>
  )
}
```

## Customized notifications

With version 4 of the design-system the `MessageBox` component has been improved by adding more color variants like `info`, `danger`, `success` and `warning`.  You can also put children in this component to see the extra paragraph below the title.

By default notifications from the hook `useNotice` are set to the `info` variant and they show translated messages from `messages` in locale by code. The example below shows how interpolation can work with a simple notice:

```typescript
// locale
{
  messages: {
    someErrorKey: "Message with interpolation {{ someParams }}" 
  }
}

// component
const addNotice = useNotice();

addNotice({
  message: 'someErrorKey',
  options: {
    someParams: ['param1', 'param2'].join(', '),
  },
})
```

As you can see you can put i18n options in `options` the param to notice. Please notice that you should use the message's keys from the locale but you can also use the message as a fallback.

We have extended the `AppError` instance with notification options to allow the backend to manipulate translation options or notification variants.

```typescript
throw new AppError('someErrorKey', { someData: 'some data for UI' }, { options: { someParams: ['param 1', 'param 2'].join(', ') }})
```

## Demo

You can find all the changes described above in our [demo application](https://demo.adminjs.co).


# Migration Guide v7

AdminJS v7 comes with a lot of new features but also many potential breaking changes. If you're a developer that started working on an AdminJS project prior to the version 7 release, this guide should help you with the migration.

## Compatibility List

Below you can find a list of AdminJS packages that are compatible with version 7 changes.

* `adminjs` (7.x.x)
* `@adminjs/design-system` (4.x.x)
* `@adminjs/express` (6.x.x)
* `@adminjs/fastify` (4.x.x)
* `@adminjs/hapi` (7.x.x)
* `@adminjs/koa` (4.x.x)
* `@adminjs/nestjs` (6.x.x)
* `@adminjs/mikroorm` (3.x.x)
* `@adminjs/mongoose` (4.x.x)
* `@adminjs/objection` (2.x.x)
* `@adminjs/prisma` (4.x.x)
* `@adminjs/sequelize` (4.x.x)
* `@adminjs/sql` (2.x.x)
* `@adminjs/typeorm` (5.x.x)
* `@adminjs/passwords` (4.x.x)
* `@adminjs/logger` (5.x.x)
* `@adminjs/import-export` (3.x.x)
* `@adminjs/upload` (4.x.x)
* **NEW** `@adminjs/themes` (1.x.x)

## ESM Support

With version 7 AdminJS has fully moved to ESM and will no longer support CJS projects.

We do not provide any specific migration tutorial for migrating to ESM, we suggest just searching for one - as long as your application is migrated to ESM, AdminJS should work without issues.

{% embed url="<https://www.google.com/search?q=nodejs+migrate+to+esm>" %}

For Typescript developers, this migration might be easier since the amount of changes you have to make is much less when compared to vanilla CommonJS apps.

{% hint style="warning" %}
If you use `@adminjs/nestjs` do not update to ESM as NestJS doesn't support ESM as of 2023/04. Instead, please see the [updated guide for NestJS plugin](/installation/plugins/nest) so that you can import updated AdminJS packages into your CJS NestJS app.
{% endhint %}

## styled-components

AdminJS's design system is built with `styled-components` library which is still incompatible with ESM in its latest official release (`5.3.9`). Version 6 is still in beta - we attempted to use it and while it did work with ESM, it also did bring some additional issues. We've also attempted to use `@emotion/styled` library instead but it is also currently incompatible with ESM.

Eventually, we decided to stick to `styled-components` version `5.3.9`. Our solution was to import the library, make necessary modifications and re-export it.

What this entails, is the modifications that you must make to your custom components:

1. Do not use default import for `styled`. Use named import instead.
2. Import from `@adminjs/design-system/styled-components` instead of `styled-components`

#### Before

```typescript
import styled, { css } from 'styled-components'
import { Box } from '@adminjs/design-system'

const someCss = css`
  /* some css */
`

const StyledBox = styled(Box)`
  ${someCss}
  
  background: black;
`

// ...
```

#### After

```typescript
import { styled, css } from '@adminjs/design-system/styled-components'
import { Box } from '@adminjs/design-system'

const someCss = css`
  /* some css */
`

const StyledBox = styled(Box)`
  ${someCss}
  
  background: black;
`

// ...
```

Please note that `styled-components` are exported from a separate namespace inside `@adminjs/design-system` to avoid mixing contexts.

{% hint style="info" %}
Typescript developers might encounter issues with TS informing you that named exports (such as `createGlobalStyle`) cannot be found in `@adminjs/design-system/styled-components`. These exports are actually present and we could not pinpoint the exact issue which is causing that error to appear.

As a workaround, you can follow the steps below to resolve the problem.

Install `@types/styled-components` as a `devDependency` in your project:

`$ yarn add -D @types/styled-components`

Create a custom `d.ts` file which extends `@adminjs/design-system/styled-components` - for example `./vendor-types/adminjs-styled-components.d.ts`:

<pre class="language-typescript"><code class="lang-typescript"><strong>declare module '@adminjs/design-system/styled-components' {
</strong>  export * from 'styled-components';
}
</code></pre>

{% endhint %}

## AdminJS.bundle removed

`AdminJS.bundle` has been deprecated since version 6. In version 7 it has been completely replaced with `ComponentLoader`. If you still haven't migrated to `ComponentLoader` , please see our custom components tutorial:&#x20;

{% content-ref url="/pages/vnHaxjeCl9izYWBhxuIp" %}
[Writing your own Components](/ui-customization/writing-your-own-components)
{% endcontent-ref %}

With version 7, we have also updated other `@adminjs/*` libraries. The libraries which bundle their own custom components now require you to pass your `ComponentLoader` instance, an example would be `@adminjs/passwords`

```typescript
import { ComponentLoader } from 'adminjs'
import passwordsFeature from '@adminjs/passwords'

import { User } from './user.entity.js'

// ...

const componentLoader = new ComponentLoader()

const admin = new AdminJS({
  componentLoader,
  resources: [{
    resource: User,
    options: {},
    features: [passwordsFeature({
      componentLoader,
      // the rest of the feature's config
    })]
  }]
})
```

The example above applies to all other features which use `ComponentLoader`.

## Overriding the Login page

Previously in order to override the Login page, you had to create an AdminJS instance and then use `overrideLogin` method:

```typescript
admin.overrideLogin({ component: LoginComponent })
```

In version 7 all components are now overridden in the same way, the login page is no exception:

```typescript
componentLoader.override('Login', <path to your login component>)
```

Prior to version 7, there were some additional drawbacks to overriding the Login page, mostly to it being Server Side Rendered, thus limiting your options for customization. In version 7, the Login page is a Single Page Application that allows you to modify the Login page freely.

## Login page translations

The default Login page now uses different translation keys than before. All its translations have been moved from either `messages`, `properties`, `labels` to its own space in `components` section.

```json
  "components": {
    "Login": {
      "welcomeHeader": "Welcome",
      "welcomeMessage": "to AdminJS - the world's leading open-source auto-generated admin panel for your Node.js application that allows you to manage all your data in one place",
      "properties": {
        "email": "Email",
        "password": "Password"
      },
      "loginButton": "Login"
    }
  },
```

To learn more about localization changes, please navigate to the Internationalization section:

{% content-ref url="/pages/DV938i7C5ob38o2Ew5bm" %}
[Internationalization (i18n)](/tutorials/internationalization-i18n)
{% endcontent-ref %}

## Design System

There are a few significant changes related to the design-system.&#x20;

#### General changes

The main color (`primary100`) is set to buttons, links, avatar, navigation, table row selections, and illustrations.

All the changes can be found in our demo application (navigate to `Pages` > `Design System Examples`):

{% embed url="<https://adminjs-demo.herokuapp.com/admin>" %}
Demo
{% endembed %}

#### Changed values for variant and color props

To simplify theme customization in AdminJS we modified available props for `Button` and `Box` components from `@adminjs/design-system`. These props are: `variant` and `color`

Previously, you could use the following values for `variant`: `primary, secondary, danger` and `success`. These values were relevant to color. Currently, `variant` attribute modifies the appearance of a button: `text, outlined, contained` and `light`.  Previous `variant` values have been moved to the new `color` attribute. `Button` default values  are: `variant="text"` and `color="primary"`.

`Box` variant attributes are: `card`, `container`, `transparent`, `grey` or `white`. The `color` attribute can be assigned similarly to how it's done with `Button`.

#### Before

```jsx
<Button variant="primary"> 
  Click me
</Button>
```

#### After

```jsx
<Button variant="contained" color="primary">
  Click me
</Button>
```

#### Disclaimer

There is a chance that some packages (React Select, Tip Tap) might not include type definitions. This is caused by their lack of support for ESM.

## Localization

The biggest change to the internationalization feature is the removal of translations from the backend and moving it client-side. The backend is currently responsible for returning the payload with the translation codes. We have made a few changes in the `locale` object in the AdminJS config. The configuration has been expanded with a couple of new options:&#x20;

* `availableLanguages`&#x20;
* `localeDetection`&#x20;
* `withBackend`

AdminJS contains core translations in a few languages. The default language is set to `en`.

The locale object can be empty (null), however, if you want to provide the user with the ability to dynamically change the language, you will have to provide the `availableLanguages` key with an array of translations.

```javascript
locale: { 
  language: 'pl', // default language
  availableLanguages: ['en', 'pl'], 
}
```

Users can use the last selected language stored in the browser cache by setting `localeDetection` variable to `true`,

```javascript
locale: { 
  language: 'pl', 
  availableLanguages: ['en', 'pl'], 
  localeDetection: true, 
}
```

You can extend or change the default translations by passing the language `translations` object into `locale` config object. Below is a simple example:

```javascript
locale: { 
  language: 'pl', 
  availableLanguages: ['en', 'pl'], 
  localeDetection: true, 
  translations: { 
    pl: { 
      messages: { 
        welcomeOnBoard_title: 'Nowy tytuł pulpitu', 
      }, 
    }, 
    en: { 
      messages: { 
        welcomeOnBoard_title: 'New dashboard title', 
      }, 
    }, 
  }, 
},
```

#### Translations groups

All the translation keys remain the same except for two new ones:

* **components** - translations for components&#x20;
* **pages** - translations for custom pages

#### Translations in custom components

You can now dynamically translate custom components and pages with  AdminJS config.

See the example based on a simple custom component:

```typescript
// ... 
import { useTranslation } from 'adminjs'
// ...

const CustomComponent = (props) => {
    const { translateComponent } = useTranslation()
    return <div>{translateComponent('CustomComponent.textToTranslate')}</div>
}
```

You have to add translations to the component's namespace in our locale config to make this work.

```javascript
const options = {
  // ...
  locale: {
    translations: {
      en: {
        components: {
          CustomComponent: {
            textToTranslate: 'This is text to translate'
          },
        }
      }
    }
  }
  // ...
}
```

If you would like to create custom messages to be handled by `useNotice` hook, you have to add our component section to the `messages` namespace. (use  above `options` data)

```javascript
import { useNotice } from "adminjs"
// ...
const sendNotice = useNotice()
// ...
sendNotice({
  message: 'Invalid "type" for relation',
  type: 'error',
})
```

If you want to use your own locale config you will have to change the translation object to be in line with the version 7 update. Below you find a simple example of what has to be changed (let's assume that the default language is `pl` and additional is `en`)

#### Before

```typescript
locale: {
  language: 'pl',
  translations: {
    labels: {
      dashboard: 'Strona główna',
    }
  }
}
```

#### After

```typescript
locale: {
  language: 'pl',
  availableLanguages: ['pl', 'en'],
  translations: {
    pl: {
      labels: {
        dashboard: 'Strona główna',
      }
    },
    en: {
      labels: {
        dashboard: 'Main page',
      }
    },
  }
}
```

## @adminjs/bundler

`@adminjs/bundler` is a library which allows you to prebundle all AdminJS browser assets:

* `app.bundle.js`&#x20;
* `design-system.bundle.js`
* `global.bundle.js`
* `components.bundle.js`

It is especially useful when you are deploying AdminJS to a server which doesn't grant you write access which AdminJS needs by default to create `components.bundle.js` which contains your custom components. `@adminjs/bundler` is a package which exports an in-code `bundle` script which you can use to solve the mentioned issue by:

1. Pregenerating all bundle files.
2. Serving them as static files on your server OR uploading them to a public storage such as AWS S3.

With the release of version 7, this package is now ESM-only and `ComponentLoader` support has been added. The script's usage has been simplified because we'd removed `customComponentsInitializationFilePath` and `adminJsOptions`. You now simply have to provide your `componentLoader` instance. Example:

```typescript
import { bundle } from '@adminjs/bundler';

import componentLoader from './component-loader.js';

(async () => {
  const files = await bundle({
    componentLoader,
    destinationDir: 'public', // relative to CWD
  });
})();
```

Now either serve the contents of `destinationDir` publicly on your server, example for Express:

```typescript
app.use(express.static(path.join(process.cwd(), 'public')));
```

or upload `destinationDir` contents to a storage of your choice (it has to be publicly accessible).

Now, remember to set `assetsCDN` option in your `AdminJS` configuration:

```typescript
// ...
const admin = new AdminJS({
  // ...,
  assetsCDN: '<PUBLIC_ASSETS_URL>'
})
```

## Demo Application

Our demo application at <https://demo.adminjs.co/admin/login> contains all the changes described above. If you're still struggling with the migration, you can take a look at a pull request which introduces these changes to our demo app: [https://github.com/SoftwareBrothers/adminjs-example-app/pull/68](https://github.com/SoftwareBrothers/adminjs-example-app/pull/68/)


# Resource

Resource is something that you can manage in AdminJS and it comes with CRUD actions (Create, Read, Update, Delete) provided out of the box.

## Introduction

Resource is something that you can manage in AdminJS and it comes with CRUD actions *(Create, Read, Update, Delete)* provided out of the box. Most of the time it is a model from your ORM or ODM.

The idea of AdminJS is to allow you to manage resources of all kinds, be it your ORM/ODM models or your custom REST API endpoints if you decide to create an [adapter ](/installation/adapters)for them.

## Adapters

AdminJS allows you to define resources through adapters. Adapters are AdminJS extensions which provide the API to communicate with your ORM, ODM or any other kind of storage or API of your choice. All adapters must extend three base classes by implementing their methods:

* [BaseResource](https://adminjs.page.link/base-resource-code) - it's responsible for CRUD operations on every resource (model) that your provide
* [BaseDatabase](https://adminjs.page.link/base-database-code) - it's responsible for loading all resources (models) defined in your database (if you choose to do so)
* [BaseProperty](https://adminjs.page.link/base-property-code) - it's responsible to describe your resource's (model's) attributes based on your model's metadata

The list of adapters that are officially supported by AdminJS can be found in[ Adapters](/installation/adapters) section.

In order to use an adapter in your documentation you must first register it.

```typescript
import AdminJS from 'adminjs'
import { Database, Resource } from '@adminjs/typeorm' // or any other adapter

AdminJS.registerAdapter({ Database, Resource })
```

You can register as many adapters as you need.

## Passing resources to AdminJS

There are two options which you can choose to provide resources to AdminJS:

1. Provide entire database connection
2. Provide every resource explicitly

### Providing entire database

This option allows you to provide an entire database and AdminJS will load all models that you have defined. However, this option may not be available for every adapter. The adapter must expose a connection or client which exposes the metadata of all models that it had loaded. `@adminjs/objection` is an example of an adapter which **does not** allow you to provide an entire database.

In order to provide an entire database for AdminJS to load, you must specify `databases` property when setting up your AdminJS instance. In case of `@adminjs/mongoose` it would look as follows:

```typescript
const mongooseDb = await mongoose.connect('mongodb://localhost:27017/test')

const admin = new AdminJS({
  databases: [mongooseDb],
})
```

For other adapters the setup would be basically the same, you just have to pass your ORM/ODM connection (or a client).

### Providing resources explicitly

This option requires you to define all resources explicitly. It is also the recommended approach since it allows you to customize every resource. In order to define resources, you must specify `resources` property when setting up your AdminJS instance. Example:

```typescript
import User from './user.entity.js'
import Profile from './profile.entity.js'

// User and Profile are models defined in your ORM/ODM

const admin = new AdminJS({
  resources: [
    User, // you can simply pass a model
    {
      resource: Profile,
      options: { // or you can provide an object with your custom resource options
        id: 'profiles', // here the resource identifier has been renamed to "profiles"
      },
    },
  ],
})
```

The way you provide resources may differ between adapters. Make sure you read a detailed tutorial for an [adapter](/installation/adapters) which you are using.

## Customizing resources

While AdminJS provides default CRUD for your application, you may want to further customize your resources. This can be done by using object definition of a resource and specifying the [ResourceOptions](https://adminjs.page.link/resource-options-code). The example above is the simplest possible where we change the `id` of a resource to `profiles`. Below you will find several examples of resource customization.

### Nesting a resource under collapsible navigation

This can be achieved by specifying `navigation` property in your resource's options, example:

```typescript

const usersNavigation = {
  name: 'Users',
  icon: 'User',
}

const admin = new AdminJS({
  resources: [{
    resource: Profile,
    options: {
      navigation: usersNavigation,
    },
  }],
})
```

This will put the `Profile` resource under `Users` menu.

### Changing visibility of properties

If you want specific properties be displayed or usable in a given action, there are two options to achieve this:

1. Set `isVisible` option for every property
2. Set `listProperties`, `editProperties`, `filterProperties`, `showProperties` in your resource

#### Setting \`isVisible\`

Every property that you have defined in your database model can be further customized in AdminJS. In this example we will hide `bio` property in `list` action and hide it from filters, but we will leave it enabled in `show` and `edit`:

```typescript
const admin = new AdminJS({
  resources: [{
    resource: Profile,
    options: {
      properties: {
        bio: {
          isVisible: {
            edit: true,
            show: true,
            list: false,
            filter: false,
          },
        },
      },
    },
  }],
})
```

#### Settings lists of visible properties

The example with `bio` hidden in `list` and `filter` but visible in `show` and `edit` can also be achieved by setting `listProperties`, `editProperties`, `filterProperties`, `showProperties`

```typescript
const admin = new AdminJS({
  resources: [{
    resource: Profile,
    options: {
      listProperties: ['id', 'name', 'createdAt'],
      filterProperties: ['id', 'name', 'createdAt'],
      editProperties: ['id', 'name', 'bio', 'createdAt'],
      showProperties: ['id', 'name', 'bio', 'createdAt'],
    },
  }],
})
```

The end result is the same but you should take note that this approach takes precedence over setting `isVisible` or property's `position`.

More examples of properties' customization can be found in [Property](/basics/property) section.

### Customizing actions

Please refer to [Action](/basics/action) section for examples and explanation.

### Changing default navigation link

By default, when you press a resource link in the sidebar, it will navigate to resource's `list` action. This can be changed by configuring `href` option. Let's say we want `users` resource to open an already-filtered `users` list which show only users that have "active" status:

```typescript
const UserResource = {
  resource: User,
  options: {
    id: 'users',
    href: ({ h, resource }) => {
      return h.resourceActionUrl({
        resourceId: resource.decorate().id(),
        actionName: 'list',
        params: {
          'filters.status': 'active',
        },
      })
    },
  },
}
```

### Configuring sorting for the resource

By default, when you navigate to your resource's `list` action it will show you results in random order since the actual database query will be lacking sorting information (unless you use table UI to select a column to sort by). You can, however, define default sorting for your resource by specifying `sort` option:

```typescript
const UserResource = {
  resource: User,
  options: {
    sort: {
      sortBy: 'updatedAt',
      direction: 'desc',
    },
  },
}
```

In the example above, we specified User resource to be sorted by it's `updatedAt` property with the latest records appearing at the top of the results list.

### Resource translations

You can define translations for your resources by specifying `locale`. This is not done on resource-level though, but during the instantiation process of AdminJS.&#x20;

#### Renaming a resource in sidebar

In order to rename your resource in the sidebar of the application, you have to set it's label:

```typescript
const admin = new AdminJS({
  resources: [User],
  locale: {
    language: 'en',
    translations: {
      labels: {
        User: 'People',
      },
    },
  },
})
```

In the example above, the `User` resource in the sidebar has been renamed to `People`.

Another scenario would be where you would want to have different messages shown based on which resource the user is currently viewing. An example would be a message which appears when you enter a resource without any records to show in `list` action:

> There are no records in this resource

In order to change it you have to define resource-specific translation as in the example below:

```typescript
const admin = new AdminJS({
  resources: [User],
  locale: {
    language: 'en',
    translations: {
      resources: {
        User: {
          messages: {
            noRecordsInResource: 'There are no users to display'
          },
        },
      },
    },
  },
})
```

To see a list of all available locales and predefined translations, please visit [locales](https://adminjs.page.link/locale-code) in `adminjs` core repository.

### Using "features"

Features are predefined pieces of code which you can import into your resource's `features`  and they will be merged with the rest of it's configuration. An example of a `feature` is `@adminjs/passwords`. This is a package which handles passwords hashing in `edit` form and shows a corresponding UI.

Usage example:

```typescript
import passwordsFeature from '@adminjs/passwords'
import argon2 from 'argon2'

import { User } from './user.entity.js'

const UserResource = {
  resource: User,
  features: [passwordsFeature({
    properties: { encryptedPassword: 'hashedPassword' },
    hash: argon2.hash,
  })],
}
```


# Action

Actions are responsible for both displaying proper views and as well handling the logic behind them.

## Introduction

AdminJS has 7 major default actions defined for each resource. Every action that is present in AdminJS panel (be it a default or a custom action) also has an automatically generated [REST API endpoint](/basics/api): `/resources/{resourceId}/actions/{action}`

All AdminJS actions can be categorised into:

* resource actions,
* record actions,
* bulk actions.

Resource actions can be accessed in the header of a content view in a given resource (above `Filter` button).

Record actions can be accessed either via a three-dot menu next to a record in the list or above the content in another record action.

Bulk-type actions appear in the list action's table header when you select at least one record.

<figure><img src="/files/QYX5oUyZE4kAKPcIEpaZ" alt=""><figcaption><p>Default actions placement</p></figcaption></figure>

### Resource-type actions

Those are the actions which don't require `recordId` parameter, meaning they are related to your resource as a whole and not to a specific record.

* `list` is responsible for listing records under a resource as well as filtering them
* `search` allows you to search records in a given resource by a query string (by default it's the title property)
* `new` is responsible for creating a new record in a given resource

### Record-type actions

Those are the actions which require `recordId` parameter. They are related to a specific record.

* `show` is responsible for showing the details of a record
* `edit` allows you to modify a specified record
* `delete` is responsible for deleting single records

### Bulk-type actions

Those are the actions which require `recordIds[]` parameter which is a list of records' ids.

* `bulkDelete` removes all selected records from the database

## Customizing actions

Each default AdminJS action can be fully customized and you are also able to create your new custom [actions](https://adminjs.page.link/action-interface-code).

### Creating custom actions

Creating a new action in AdminJS is very simple. This can be achieved by simply adding a new action key into resource's options' `actions` configuration:

```typescript
const UserResource = {
  resource: User,
  options: {
    actions: {
      myCustomAction: {
        actionType: 'record',
        component: false,
        handler: (request, response, context) => {
          const { record, currentAdmin } = context
          return {
            record: record.toJSON(currentAdmin),
            msg: 'Hello world',
          }
        },
      },
    },
  },
}
```

Every custom action requires you to specify `actionType` which can be `record`, `resource` or `bulk`. You also must define a `handler` for your action. This is a backend method for fetching or processing your data. A single `handler` is used for both querying and mutating processes but you are able to differentiate these based on `request.method` (which is `post` or `get`).

The `handler` must return `records` in case of `bulk` or `resource` actions or `record` for `record` actions. You can return any additional information alongside `record`/`records`.

The last important option is `component`. This can either be `false` which means that the backend `handler` will be triggered once you press the button or you can provide your custom component that will be rendered.

#### Action with custom component

{% code title="my-custom-action.tsx" %}

```jsx
import React from 'react'
import { Box, H3 } from '@adminjs/design-system'
import { ActionProps } from 'adminjs'

const MyCustomAction = (props: ActionProps) => {
  const { record } = props

  return (
    <Box flex>
      <Box variant="white" width={1 / 2} boxShadow="card" mr="xxl" flexShrink={0}>
        <H3>Example of a simple page</H3>
        <p>Where you can put almost everything</p>
        <p>like this:</p>
        <p>
          <img src="https://i.redd.it/rd39yuiy9ns21.jpg" alt="stupid cat" width={300} />
        </p>
      </Box>
      <Box>
        <p>Or (more likely), operate on a returned record:</p>
        <Box overflowX="auto">{JSON.stringify(record)}</Box>
      </Box>
    </Box>
  )
}

export default MyCustomAction
```

{% endcode %}

Resource options code:

```typescript
const UserResource = {
  resource: User,
  options: {
    actions: {
      myCustomAction: {
        actionType: 'record',
        component: Components.MyCustomAction, // see "Writing your own Components"
        handler: (request, response, context) => {
          const { record, currentAdmin } = context
          return {
            record: record.toJSON(currentAdmin),
            msg: 'Hello world',
          }
        },
      },
    },
  },
}
```

In addition to the required configuration, you may also specify other optional options. They will be covered in the later parts of this section.

### Changing visibility and accessibility of an action

In AdminJS all actions are both accessible and visible by default. By saying that an action is visible, it means you can access it in the UI. An accessible action is an action which can be accessed via AdminJS API even if it is not visible.

You can change the visibility of an action by using `isVisible` option or change it's accessibility by using `isAccessible` option. Their configuration is the same, they can be just a boolean value:

```typescript
const UserResource = {
  resource: User,
  options: {
    actions: {
      edit: {
        isAccessible: false,
        isVisible: true,
      },
    },
  },
}
```

They can also be an `IsFunction` which allows you to access record's context:

```typescript
const UserResource = {
  resource: User,
  options: {
    actions: {
      edit: {
        isAccessible: (context) => {
          const { record, currentAdmin } = context
          
          // We are only allowing to edit records created by currently logged in user
          return record?.params?.createdByUserId === currentAdmin.id
        },
        isVisible: true,
      },
    },
  },
}
```

In case of `record` actions the accessibility and visibility checks are done in runtime, for example when loading a specific record. `Resource` actions' accessibility and visibility is evaluated when your AdminJS instance is created.

### Displaying a Filter button in your resource action

The `Filter` button which you may have noticed above the table of the default `list` action can also be shown on your own `resource` actions' pages. However, by default it is disabled. If you would like the `Filter` button to be present you should use `showFilter` option. The filters work exactly the same as in the `list` action - when you set them, they are appended to your browser's address and it is up to you to process assigned filters in your resource action's component.

The `showFilter` option can also be used to hide the filters for `list` action.

```typescript
const UserResource = {
  resource: User,
  options: {
    actions: {
      list: {
        showFilter: false,
      },
    },
  },
}
```

### Displaying a confirmation message when you press an action button

This is a feature which you might have seen when attempting to delete a record in AdminJS [demo application](https://adminjs-demo.herokuapp.com/admin/login). Once you press `Delete`, a browser's confirmation pop-up appears asking you to confirm the removal of a record. If you want a similar behaviour in your custom actions, you should use `guard` option.

```typescript
const UserResource = {
  resource: User,
  options: {
    actions: {
      myCustomAction: {
        actionType: 'record',
        component: false,
        handler: (request, response, context) => {
          const { record, currentAdmin } = context
          return {
            record: record.toJSON(currentAdmin),
          }
        },
        guard: 'doYouReallyWantToDoThis',
      },
    },
  },
}
```

The `guard` message goes through localization so you can use a translation key.

### Using "before" and "after" hooks

In addition to `handler` you can also define `before` and `after` hooks in your custom or existing actions. The `before` hook is triggered at the start of an action, before `isAccessible` and `handler`. `after` hook, in turn, is triggered at the end of an action.&#x20;

In the example below, we will use `before` hook in `list` action to set default filter that only shows `User` resource results which have `status` equal to `active`. Additionally, we will use `after` hook to `console.log` the `meta` property of the response returned by `handler`.

```typescript
const customBefore = (request, context) => {
  const { query = {} } = request
  const newQuery = {
    ...query,
    ['filters.status']: 'active',
  }
  
  request.query = newQuery
  
  return request
}

const customAfter = (originalResponse, request, context) => {
  console.log(originalResponse.meta)
  
  return originalResponse
}

const UserResource = {
  resource: User,
  options: {
    actions: {
      list: {
        before: [customBefore],
        after: [customAfter],
      },
    },
  },
}
```

The `before` and `after` options accept a list of `Before` and `After` functions. This means you can assign multiple `before`/`after` hooks to your action where their order is important because every hook uses the arguments that could have been modified by the previous ones.

`before` hook can also be used to add additional validation for example in your `edit` action:

```typescript
// other imports
import { ValidationError } from 'adminjs'

const validateForm = (request, context) => {
  const { payload = {}, method } = request
  
  // We only want to validate "post" requests
  if (method !== 'post') return request
  
  // Payload contains data sent from the frontend
  const { age = null, lastName = '' } = payload
  
  // We will store validation errors in an object, so that
  // we can throw multiple errors at the same time
  const errors = {}
  
  // We are doing validations and assigning errors to "errors" object
  if (!age || age < 18) {
    errors.age = {
      message: 'A user must be at least 18 years old',
    }
  }
  
  if (lastName.trim().length === 0) {
    errors.lastName = {
      message: 'Last name is required',
    }
  }
  
  // We throw AdminJS ValidationError if there are errors in the payload
  if (Object.keys(errors).length) {
    throw new ValidationError(errors)
  }
  
  return request
}

const UserResource = {
  resource: User,
  options: {
    actions: {
      edit: {
        before: [validateForm],
      },
    },
  },
}
```

The errors will now be highlighted in `edit` action's form.

### Showing record actions in a drawer

By default every record action is displayed on a new page. You can, however, display them in a drawer at the right side of the screen, similarly to how `Filters` drawer is opened. This can be achieved by using `showInDrawer` option:

```typescript
const UserResource = {
  resource: User,
  options: {
    actions: {
      myCustomAction: {
        actionType: 'record',
        component: Components.MyCustomAction, // see "Writing your own Components"
        handler: (request, response, context) => {
          const { record, currentAdmin } = context
          return {
            record: record.toJSON(currentAdmin),
          }
        },
        showInDrawer: true,
      },
    },
  },
}
```

### Nesting record actions in a context menu when there are too many of them

If you ever encounter a situation where you have defined a lot of custom record actions and they don't fit your screen width, using `parent` option can be a solution for you. This option allows you to set a name of a dropdown button which will group those actions under it.

```typescript
const UserResource = {
  resource: User,
  options: {
    actions: {
      myCustomAction: {
        actionType: 'record',
        component: false,
        handler: (request, response, context) => {
          const { record, currentAdmin } = context
          return {
            record: record.toJSON(currentAdmin),
          }
        },
        parent: 'More',
      },
      myOtherCustomAction: {
        actionType: 'record',
        component: false,
        handler: (request, response, context) => {
          const { record, currentAdmin } = context
          return {
            record: record.toJSON(currentAdmin),
          }
        },
        parent: 'More',
      },
    },
  },
}
```

In the example above, there are two custom actions: `myCustomAction` and `myOtherCustomAction` which both are grouped under `More` dropdown button.

&#x20;


# Property

Properties are AdminJS's representation of your model's fields.

## Introduction

Properties are AdminJS's representation of your model's fields. Every adapter used by AdminJS must export a `Property` which is an extension of [BaseProperty](https://adminjs.page.link/base-property-code). In this section you will learn how to override default settings of your resource's properties as well as how to add new properties to your resource using it's configuration object.

## Customizing properties

This tutorial will cover how you can use different [Property options](https://adminjs.page.link/property-options-code) to customize your properties.

### Creating custom properties

Let's assume that you would like to add a new property called `randomPicture` to your `User` resource which would show a randomly generated picture in your `User` resource's `list` and `show` actions.

First, let's create a custom React component which will display the image:

```jsx
import React from 'react'
import { ShowPropertyProps } from 'adminjs'
import { Box } from '@adminjs/design-system'

const RandomPicture: React.FC<ShowPropertyProps> = (props) => {
  // Picsum generates a random 200x200 photo
  const url = 'https://picsum.photos/200'
  
  return <img src={url} />
}

export default RandomPicture
```

<pre class="language-typescript"><code class="lang-typescript"><strong>const UserResource = {
</strong>  resource: User,
  options: {
    properties: {
      randomPicture: {
        type: 'string',
        components: {
          list: Components.MyCustomAction, // see "Writing your own Components"
          show: Components.MyCustomAction,
        },
      },
    },
  },
}
</code></pre>

Every property can be further customized. That will be covered in the later parts of this section.

### Displaying a tooltip next to a field in forms

If a field that you are displaying in your form might require additional information to fill in correctly, you may want to use a `description` option in your property's configuration. This option allows you to set a message that will be displayed after hovering over a question mark icon next to a label in your form.

```typescript
const UserResource = {
  resource: User,
  options: {
    properties: {
      links: {
        description: "User's Linkedin/Github/social profiles links",
      },
    },
  },
}
```

You can also use a translation key and define a translation in locale.

```typescript
links: {
  description: "userLinksHint",
},
```

### Defining available values for user to choose from

Although this is not required most of the time because your adapter of choice should be able to load enum values from your models, there can still be a situation where you want a field to be a selection instead of a regular text input. This can be done through `availableValues` option.

```typescript
const UserResource = {
  resource: User,
  options: {
    properties: {
      gender: {
        availableValues: [
          { value: 'male', label: 'Male' },
          { value: 'female', label: 'Female' },
          { value: 'other', label: 'Other' },
          { value: 'notSay', label: 'Rather not say' },
        ],
      },
    },
  },
}
```

### Passing props into HTML element/component

There could be a case where you want to pass extra props to the React component or HTML element which AdminJS is using to render a form field. You can use `props` for this:

```typescript
const UserResource = {
  resource: User,
  options: {
    properties: {
      bio: {
        type: 'textarea',
        props: {
          rows: 20,
        },
      },
    },
  },
}
```

The example above sets [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#attr-rows)'s `rows` prop to `20`, the default being `2`.


# Features

This section contains detailed instructions on how to use AdminJS `features`.

Features are ready-made extensions for your resources. They extend existing resource options with predefined configuration and merge with your custom actions or properties configurations.

Features should be included in `features` section of your resource, example:

<pre class="language-typescript"><code class="lang-typescript"><strong>import { ResourceWithOptions } from 'adminjs';
</strong><strong>
</strong><strong>import User from './user.entity.js';
</strong><strong>
</strong><strong>const UserResource: ResourceWithOptions = {
</strong><strong>  resource: User,
</strong><strong>  options: {},
</strong><strong>  features: [someFeature({ /* feature config */ })],
</strong>};

export default UserResource;
</code></pre>

The most common use case of features is when you want some specific behaviour or configuration to be shared by multiple resources, for example: you may want to create a feature which logs changes to server's console.

If you'd like to learn how to write your own `features`, please visit:

{% content-ref url="/pages/TDPQKQ6yDS9Tom1nf5oo" %}
[Writing your own features](/basics/features/writing-your-own-features)
{% endcontent-ref %}


# Relations

@adminjs/relations

`@adminjs/relations` is a feature which allows you to manage `one-to-many` and `many-to-many` relations within your admin panel.

As of version `1.0.0` it supports:

* listing multiple `one-to-many` relations with pagination but no filters in the details view of a record,
* listing multiple `many-to-many` relations with pagination but no filters in the details view of a record,
* editing and creating `one-to-many` relations,
* editing `many-to-many` relations,
* deleting records listed in `one-to-many` table (if you want to only remove the relation, you can just modify the target record)
* deleting relations in junction table of `many-to-many` relation **or** deleting the target record of a `many-to-many` relation (it also deleted the relation in junction table)
* navigating to details view of a target relation,
* adding an existing record to a `many-to-many` relation or creating a new record which will be assigned to your `many-to-many` relation.

<div><figure><img src="/files/DspVYR0NfRhMxcOmUqSP" alt=""><figcaption><p>One-To-Many List</p></figcaption></figure> <figure><img src="/files/6ppZCpVUQ81IP8wCevVC" alt=""><figcaption><p>Many-To-Many List</p></figcaption></figure> <figure><img src="/files/toMEpvaqRTqs4aQXE20s" alt=""><figcaption><p>Many-To-Many Modal</p></figcaption></figure></div>

## Installation

`@adminjs/relations` is a premium feature which can be purchased at <https://cloud.adminjs.co>

All premium features currently use **One Time Payment** model and you can use them in all apps that belong to you. Once you purchase the addon, you will receive a license key which you should provide in `@adminjs/relations` configuration in your application's code.

Installing the library:

```bash
$ yarn add @adminjs/relations
```

The license key should be provided to `owningRelationSettingsFeature`:

```typescript
owningRelationSettingsFeature({
  licenseKey: process.env.LICENSE_KEY,
  // the rest of the config
})
```

`targetRelationSettingsFeature` does not require a license key as it's role is mostly utility-only. The documentation below describes the configuration objects and setup instructions in more detail.

If you encounter any issues or require help installing the package please contact us at <adminjs@adminjs.co> or through our Discord server.

## Usage

Similarly to other features, the `@adminjs/relations` feature has to be imported into `features` configuration section of your resource. `@adminjs/relations` exports two separate feature that you must configure in order for the functionality to work:

* `owningRelationSettingsFeature` is used to configure the relations that you will want to manage later,
* `targetRelationSettingsFeature` doesn't require any configuration, but it has to be included in targetted resource in order for redirects and `many-to-many` assignments to work properly.

The two features will be explained in more detail in the later parts of this guide.

### Example Database Structure

The usage guide will be based on sample database tables which can be represented by the following interfaces:

{% tabs %}
{% tab title="Generic" %}

```typescript
interface IOrganization {
  id: number;
  name: string;
}

interface ITeam {
  id: number;
  name: string;
}

interface IPerson {
  id: number;
  name: string;
  email: string;
  organizationId: number;
}

interface ITeamMember {
  id: number;
  personId: number;
  teamId: number;
}

interface IOffice {
  id: number;
  name: string;
  address: string;
  organizationId: string;
}

/*
  Person belongs to 1 Organization
  Organization has many Persons
  Office belongs to 1 Organization
  Organization has many Offices
  Person belongs to multiple Teams through TeamMember
  Team belongs to multiple Persons through TeamMember
*/
```

{% endtab %}

{% tab title="Prisma Schema" %}

```prisma
generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
}

model Organization {
    id      Int      @id @default(autoincrement())
    name    String
    persons Person[]

    @@map("organizations")
}

model Person {
    id                  Int          @id @default(autoincrement())
    firstName           String       @map("first_name")
    lastName            String       @map("last_name")
    email               String
    phone               String
    dateOfBirth         DateTime?     @map("date_of_birth")
    isActive            Boolean
    organization        Organization @relation(fields: [organizationId], references: [id])
    organizationId      Int          @map("organization_id")

    teams               TeamMember[]

    @@map("persons")
}

model Team {
    id                  Int         @id @default(autoincrement())
    name                String
    members             TeamMember[]

    @@map("teams")
}

model TeamMember {
    id                  Int         @id @default(autoincrement())
    personId            Int         @map("person_id")
    person              Person      @relation(fields: [personId], references: [id])
    teamId              Int         @map("team_id")
    team                Team        @relation(fields: [teamId], references: [id])

    @@map("team_members")
}
```

{% endtab %}
{% endtabs %}

`@adminjs/relations` is adapter-agnostic which means you can use it regardless of the database adapter you had installed. Nevertheless, some ORMs automatically generate and manage  junction tables for you without you having to actually create entities for them in your codebase. This will not work with AdminJS and you will have to create actual entities for junction tables and register them as AdminJS resources since AdminJS uses them to find your `M:N` records.

```typescript
const admin = new AdminJS({
  resources: [
    createOrganizationResource(),
    createPersonResource(),
    createOfficeResource(),
    createTeamResource(),
    createTeamMemberResource(),
  ],
})
```

{% hint style="warning" %}
AdminJS requires every resource to have a primary key column, this includes junction tables.
{% endhint %}

### Feature Options

Below you can find feature options of `owningRelationSettingsFeature` which you can use for reference.

```typescript
enum RelationType {
  OneToMany = 'one-to-many',
  ManyToMany = 'many-to-many',
}

type RelationsFeatureConfig = {
  /* Your ComponentLoader instance, ideally you will create it in a separate file
  and import where it's needed. Documentation: https://docs.adminjs.co/ui-customization/writing-your-own-components */
  componentLoader: ComponentLoader;
  /* Your license key */
  licenseKey: string;
  /* A configuration object for relations that will be managable in a given resource. */
  relations: {
    /* A name of a relation. It will be used as a name in tabbed table (see screenshots above) */
    [resourceId: string]: {
      /* A relation type which can be either `one-to-many` or `many-to-many` */
      type: RelationType;
      /* A junction resource/table configuration. It is only required for `many-to-many` */
      junction?: {
        /* A "joinKey" inside junction table. If configuring for "Team", it can be "teamId". */
        joinKey: string;
        /* An "inverseJoinKey" inside junction table. If "Team" has a M:N relation with "Person", it can be "personId" */
        inverseJoinKey: string;
        /* A resource ID of the junction table, for example: "TeamMember" */
        throughResourceId: string;
      };
      /* A target resource/table configuration. A target is a resource which is listed in the table. */
      target: {
        /* A "resourceId" of the target. Example: "Person" */
        resourceId: string;
        /* A "joinKey" of the target. Example: "organizationId" */
        joinKey?: string;
      };
    }
  };
  /* An optional field which allows you to specify a different property key which will be used
  to display relations table. By default it adds `relations` to details view of your resource. */
  propertyKey?: string;
};
```

### One-To-Many

{% hint style="warning" %}
If using Prisma, configure the `joinKey` and `inverseJoinKey` options by providing the relation names instead of foreign keys, for example: `organization` instead of `organizationId`
{% endhint %}

According to the database structure described above as well as the presented configuration options of `owningRelationSettingsFeature`, this is how you can add this feature to `Organization` resource which can have many `Persons` and `Offices`

{% code title="organization.resource.ts" %}

```typescript
import { owningRelationSettingsFeature, type RelationType } from '@adminjs/relations'
import { componentLoader } from './component-loader.js';
import { Organization } from './models/index.js';

export const createOrganizationResource = () => ({
  resource: Organization,
  features: [
    owningRelationSettingsFeature({
      componentLoader,
      licenseKey: process.env.LICENSE_KEY,
      relations: {
        persons: {
          type: RelationType.OneToMany,
          target: {
            joinKey: 'organizationId',
            resourceId: 'Person',
          },
        },
        offices: {
          type: RelationType.OneToMany,
          target: {
            joinKey: 'organizationId',
            resourceId: 'Office',
          },
        },
      },
    }),
  ],
});
```

{% endcode %}

Additionally, in your `Office` and `Person` resources you will have to add `targetRelationSettingsFeature`:

{% code title="office.resource.ts" %}

```typescript
import { targetRelationSettingsFeature } from '@adminjs/relations';
import { Office } from './models/index.js';

export const createOfficeResource = () => ({
  resource: Office,
  features: [targetRelationSettingsFeature()],
});
```

{% endcode %}

{% code title="person.resource.ts" %}

```typescript
import { targetRelationSettingsFeature } from '@adminjs/relations';
import { Person } from './models/index.js';

export const createPersonResource = () => ({
  resource: Person,
  features: [targetRelationSettingsFeature()],
});
```

{% endcode %}

If you configure your resources as shown above, you should be able to see `Persons` and `Offices` tabs in your `Organization` record's details view.

### Many-To-Many

The example below shows how you can configure a `many-to-many` relation between `Team` and `Person` through `TeamMember`.

{% code title="team.resource.ts" %}

```typescript
import { owningRelationSettingsFeature, type RelationType } from '@adminjs/relations';
import { Team } from './models/index.js';
import { componentLoader } from './component-loader.js';

export const createTeamResource = () => ({
  resource: Team,
  options: {
    navigation: { icon: 'Users' },
  },
  features: [
    owningRelationSettingsFeature({
      componentLoader,
      licenseKey: process.env.LICENSE_KEY,
      relations: {
        members: {
          type: RelationType.ManyToMany,
          junction: {
            joinKey: 'teamId',
            inverseJoinKey: 'personId',
            throughResourceId: 'TeamMember',
          },
          target: {
            resourceId: 'Person',
          },
        },
      },
    }),
  ],
});
```

{% endcode %}

Additionally, in your `Person` resource you will have to make sure to add `targetRelationSettingsFeature`. Of course, if you had added it before you don't have to add it multiple times.

{% code title="person.resource.ts" %}

```typescript
import { targetRelationSettingsFeature } from '@adminjs/relations';
import { Person } from './models/index.js';

export const createPersonResource = () => ({
  resource: Person,
  features: [targetRelationSettingsFeature()],
});
```

{% endcode %}

### Role Based Access Control

By default all actions related to managing the relations will be available for everyone. You can modify the accessibility in the same way you modify accessibility of your custom actions.

`@adminjs/relations` introduces the following new actions to your resource:

* `findRelation` is used to list `one-to-many` and `many-to-many` records from the target resource,
* `addManyToManyRelation` is used to add existing records to a junction table for `many-to-many` relations
* `deleteRelation` is used to delete a record from a junction table, deleting the relation in the process, but leaving both records

Taking `Team` resource from above as an example, you can allow these actions only for users with role `Admin` by doing the following changes:

```typescript
import { owningRelationSettingsFeature, type RelationType } from '@adminjs/relations';
import { Team } from './models/index.js';
import { componentLoader } from './component-loader.js';

const onlyForAdmin = ({ currentAdmin }) => currentAdmin.role === 'Admin';

export const createTeamResource = () => ({
  resource: Team,
  options: {
    navigation: { icon: 'Users' },
    actions: {
      findRelation: { isAccessible: onlyForAdmin },
      addManyToManyRelation: { isAccessible: onlyForAdmin },
      deleteRelation: { isAccessible: onlyForAdmin },
    },
  },
  features: [
    owningRelationSettingsFeature({
      componentLoader,
      licenseKey: process.env.LICENSE_KEY,
      relations: {
        members: {
          type: RelationType.ManyToMany,
          junction: {
            joinKey: 'teamId',
            inverseJoinKey: 'personId',
            throughResourceId: 'TeamMember',
          },
          target: {
            resourceId: 'Person',
          },
        },
      },
    }),
  ],
});
```

You can read more about RBAC in AdminJS in [this tutorial](/tutorials/adding-role-based-access-control).


# Upload

@adminjs/upload

The upload feature helps organize your files and keep information about them in database.

There is possibility to use different storage for files:&#x20;

* local filesystem
* AWS S3
* Google Cloud Storage

To install the upload feature run:

<pre class="language-shell"><code class="lang-shell"><strong>$ yarn add @adminjs/upload
</strong></code></pre>

The main concept of the upload feature is that it sends uploaded files to an external source. The database keeps the information about path and folder name where the file was stored.

The feature uses following terms

* `key` is the path of the stored file
* `bucket` is the name of the container

First we have to create in our database table where we store information about our files.

Below is written interface for entity. Feel free to add your own fields to store other data connected with file.

```typescript
interface IFile {
  id: number;
  s3Key: string;
  bucket: string;
  mime: string;
  comment: string | null;
}
```

Next, you should decide, where your files will be stored and prepare resource entry for AdminJS

{% tabs %}
{% tab title="Local Filesystem" %}
In this example your local server should have established `bucket` folder and it should be accessible by web browser (via `baseUrl` path)

<pre class="language-javascript"><code class="lang-javascript"><strong>import * as url from 'url'
</strong>// other imports

const __dirname = url.fileURLToPath(new URL('.', import.meta.url))

app.use(express.static(path.join(__dirname, '../public')));
</code></pre>

```typescript
import uploadFeature from '@adminjs/upload';

import { File } from './models/file.js';
import componentLoader from './component-loader.js';

const localProvider = {
  bucket: 'public/files',
  opts: {
    baseUrl: '/files',
  },
};

export const files = {
  resource: File,
  options: {
    properties: {
      s3Key: {
        type: 'string',
      },
      bucket: {
        type: 'string',
      },
      mime: {
        type: 'string',
      },
      comment: {
        type: 'textarea',
        isSortable: false,
      },
    },
  },
  features: [
    uploadFeature({
      componentLoader, 
      provider: { local: localProvider },
      validation: { mimeTypes: ['image/png', 'application/pdf', 'audio/mpeg'] },
    }),
  ],
};
```

{% endtab %}

{% tab title="AWS S3" %}

```typescript
import uploadFeature from '@adminjs/upload';

import { File } from './models/file.js';
import componentLoader from './component-loader.js';

const AWScredentials = {
  accessKeyId: 'AWS_ACCESS_KEY_ID',
  secretAccessKey: 'AWS_SECRET_ACCESS_KEY',
  region: 'AWS_REGION',
  bucket: 'AWS_BUCKET',
};

export const files = {
  resource: File,
  options: {
    properties: {
      s3Key: {
        type: 'string',
      },
      bucket: {
        type: 'string',
      },
      mime: {
        type: 'string',
      },
      comment: {
        type: 'textarea',
        isSortable: false,
      },
    },
  },
  features: [
    uploadFeature({
      componentLoader,
      provider: { aws: AWScredentials },
      validation: { mimeTypes: ['application/pdf'] },
    }),
  ],
};
```

{% endtab %}

{% tab title="Google Cloud Storage" %}

<pre class="language-typescript"><code class="lang-typescript">import uploadFeature from '@adminjs/upload';

import { File } from './models/file.js';
import componentLoader from './component-loader.js';

<strong>const GCScredentials = {
</strong>  serviceAccount: 'SERVICE_ACCOUNT',
  bucket: 'GCP_STORAGE_BUCKET',
  expires: 0,
};

export const files = {
  resource: File,
  options: {
    properties: {
      s3Key: {
        type: 'string',
      },
      bucket: {
        type: 'string',
      },
      mime: {
        type: 'string',
      },
      comment: {
        type: 'textarea',
        isSortable: false,
      },
    },
  },
  features: [
    uploadFeature({
      componentLoader,
      provider: { gpc: GCScredentials },
      validation: { mimeTypes: ['image/png'] },
    }),
  ],
};
</code></pre>

{% endtab %}
{% endtabs %}

After that add files resource to AdminJS options config

```typescript
import { files } from './resources/files.js';

const adminJsOptions = {
  resources: [
     //...
     files
  ],
  //...
}
```

If you would like to deal with multiple files with single database entry it will be necessary to modify config files

{% tabs %}
{% tab title="Entity" %}

```typescript
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';

@Entity({ name: 'files' })
export class File extends BaseEntity {
  @PrimaryGeneratedColumn()
  public id: number;

  @Column({ name: 's3_key', nullable: true, type: 'jsonb' })
  public s3Key: string;

  @Column({ nullable: true, type: 'jsonb' })
  public bucket: string;

  @Column({ nullable: true, type: 'jsonb' })
  public mime: string;

  @Column({ nullable: true, type: 'text' })
  public comment: string;

  @CreateDateColumn({ name: 'created_at' })
  public createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  public updatedAt: Date;
}

```

{% endtab %}

{% tab title="Resource" %}

```typescript
import uploadFeature from '@adminjs/upload';

import { File } from './models/file.js';

const localProvider = {
  bucket: 'public/files',
  baseUrl: '/files',
};

export const files = {
  resource: File,
  options: {
    properties: {
      s3Key: {
        type: 'string',
        isArray: true,
      },
      bucket: {
        type: 'string',
        isArray: true,
      },
      mime: {
        type: 'string',
        isArray: true,
      },
      comment: {
        type: 'textarea',
        isSortable: false,
      },
    },
  },
  features: [
    uploadFeature({
      provider: { local: localProvider },
      multiple: true,
      validation: { mimeTypes: ['image/png', 'application/pdf', 'audio/mpeg'] },
    }),
  ],
};
```

{% endtab %}
{% endtabs %}


# Logger

@adminjs/logger

AdminJS has some extra plugins which extend its basic functionality. One of them is logger.&#x20;

The logger's purpose is to keep track of selected resources (i.e. tables) Every action performed on resource (new, edit, delete or bulkDelete) is registered in special object and persisted in database. Information about changes are monitored, so we can inspect and compare changes in every field of a table.

Installation is pretty simple. First we have to install `@adminjs/logger` package

```shell
$ yarn add @adminjs/logger
```

We can use any database and any ORM package as described in **Adapters** section.

Then we have to define `Log` entity - the place we will track changes.  Below you can find example

{% tabs %}
{% tab title="Sequelize" %}

```typescript
import { DataTypes, Model } from 'sequelize';

import db from './sequelize.connection.js';
import User from './user.entity.js';

export interface ILog = {
  id: number;
  action: string;
  resource: string;
  userId: number;
  recordId: number;
  recordTitle: string;
  difference: string;
  createdAt: Date;
  updatedAt: Date;
};

export class Log extends Model<ILog> {
  id: number;
  createdAt: Date;
  updatedAt?: Date;
  recordId: number;
  recordTitle: string | null;
  difference: Record<string, unknown> | null;
  action: string;
  resource: string;
  userId: number;
}

Log.init(
  {
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true,
    },
    action: {
      type: new DataTypes.STRING(128),
      allowNull: false,
    },
    resource: {
      type: new DataTypes.STRING(128),
      allowNull: false,
    },
    userId: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    recordId: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    recordTitle: {
      type: new DataTypes.STRING(128),
      allowNull: false,
    },
    difference: {
      type: DataTypes.JSONB,
      allowNull: true,
    },
  },
  {
    sequelize: db,
    tableName: 'logs',
    timestamps: true,
  }
);

export default Log;
```

{% endtab %}

{% tab title="TypeORM" %}

```typescript
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

export interface ILog {
  id: number;
  action: string;
  resource: string;
  userId: string | null;
  recordId: number;
  recordTitle: string | null;
  difference: Record<string, unknown> | null;
  createdAt: Date;
  updatedAt?: Date;
}

@Entity({ name: 'logs' })
export class Log extends BaseEntity implements ILog {
  @PrimaryGeneratedColumn()
  public id: number;

  @CreateDateColumn({ name: 'created_at' })
  public createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  public updatedAt?: Date;

  @Column({ name: 'record_id', type: 'integer', nullable: false })
  public recordId: number;

  @Column({ name: 'record_title', type: 'text', nullable: true, default: '' })
  public recordTitle: string | null;

  @Column({ name: 'difference', type: 'jsonb', nullable: true, default: {} })
  public difference: Record<string, unknown> | null;

  @Column({ name: 'action', type: 'varchar', length: 128, nullable: false })
  public action: string;

  @Column({ name: 'resource', type: 'varchar', length: 128, nullable: false })
  public resource: string;

  @Column({ name: 'user_id', type: 'varchar', nullable: false })
  public userId: string;
}

```

{% endtab %}

{% tab title="MikroORM" %}

```typescript
import { Entity, BaseEntity, PrimaryKey, Property } from '@mikro-orm/core';

export interface ILog {
  id: number;
  action: string;
  resource: string;
  userId: string | null;
  recordId: number;
  recordTitle: string | null;
  difference: Record<string, unknown> | null;
  createdAt: Date;
  updatedAt?: Date;
}

@Entity({ tableName: 'logs' })
export class Log extends BaseEntity<Log, 'id'> implements ILog {
  @PrimaryKey()
  public id: number;

  @Property({ columnType: 'datetime', fieldName: 'created_at', nullable: false })
  public createdAt: Date = new Date();

  @Property({ columnType: 'datetime', fieldName: 'updated_at', nullable: true })
  public updatedAt?: Date = new Date();

  @Property({ columnType: 'integer', fieldName: 'record_id', nullable: false })
  public recordId: number;

  @Property({ columnType: 'varchar', fieldName: 'record_title', nullable: true })
  public recordTitle: string | null;

  @Property({ columnType: 'jsonb', fieldName: 'difference', nullable: true })
  public difference: Record<string, unknown> | null;

  @Property({ columnType: 'varchar', fieldName: 'action', nullable: false })
  public action: string;

  @Property({ columnType: 'varchar', fieldName: 'resource', nullable: false })
  public resource: string;

  @Property({ columnType: 'varchar', fieldName: 'user_id', nullable: false })
  public userId: string;
}

```

{% endtab %}

{% tab title="Mongoose" %}

```typescript
import { model, Schema } from 'mongoose';

export interface Log {
  id: number;
  action: string;
  resource: string;
  userId: string | null;
  recordId: string;
  recordTitle: string | null;
  difference: Record<string, unknown> | null;
  createdAt: Date;
  updatedAt?: Date;
}

export const LogSchema = new Schema<Log>({
  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now },
  recordId: { type: 'String', required: true },
  recordTitle: { type: 'String' },
  difference: 'Object',
  action: { type: 'String' },
  resource: { type: 'String' },
  userId: { type: 'String' },
});

export const LogModel = model<Log>('Log', LogSchema);

```

{% endtab %}

{% tab title="ObjectionJS" %}

```typescript
import { BaseModel } from '../utils/base-model.js';

class Log extends BaseModel {
  id: number;
  recordId: number;
  recordTitle: string;
  difference: Record<string, unknown> | null;
  action: string;
  resource: string;
  userId: string;

  static tableName = 'logs';

  static jsonSchema = {
    type: 'object',
    required: ['recordId', 'action', 'resource', 'userId'],

    properties: {
      id: { type: 'integer' },
      recordId: { type: 'integer' },
      recordTitle: { type: 'string', minLength: 1, maxLength: 128 },
      difference: { type: 'jsonb' },
      action: { type: 'string', minLength: 1, maxLength: 128 },
      resource: { type: 'string', minLength: 1, maxLength: 128 },
      userId: { type: 'string', minLength: 1, maxLength: 128 },
      createdAt: { type: 'string', format: 'date-time', readOnly: true },
      updatedAt: { type: 'string', format: 'date-time', readOnly: true },
    },
  };
}

export default Log;

```

{% endtab %}

{% tab title="Prisma" %}

```
model Log {
  id          Int      @id @default(autoincrement())
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  recordId    Int
  recordTitle String?  @db.VarChar(128)
  difference  Json?    @db.Json
  action      String   @db.VarChar(128)
  resource    String   @db.VarChar(128)
  userId      String   @db.VarChar(128)

}
```

{% endtab %}
{% endtabs %}

Entity`Log` is related to entity `User` because `userId` holds reference to user who made changes&#x20;

There is one thing worth to mention. We have to enable authorization before using this feature, because we will need user's ID for corresponding changes.

To get logger to work, add extra property to resource config. Below you can find simple example of such configuration.

```javascript
import loggerFeature from '@adminjs/logger';

import ResourceModel from './resource.entity.js';
import componentLoader from './component-loader.js';

export default {
  resource: ResourceModel,
  features: [
    loggerFeature({
      componentLoader,
      propertiesMapping: {
        user: 'userId',
      },
      userIdAttribute: 'id',
    }),
  ],
};

```

To have Log resource appear in AdminJS panel, we have to define it first.&#x20;

`@adminjs/logger exports` `createLoggerResource` function which does most of the work for you. You can customize it using it's configuration argument.

```javascript
import { createLoggerResource } from '@adminjs/logger';

import Log from './logs.entity.js';

const config = {
  resource: Log,
  featureOptions: {
    propertiesMapping: {
      recordTitle: 'title' //field to store logged record's title
    ,
    userIdAttribute: 'id', //primary key currently logged user
    resourceOptions: {
      navigation: {
        name: 'SectionName',
        icon: 'iconName'
      }
    }
  }
}

export default createLoggerResource(config)
```


# Import & Export

@adminjs/import-export

AdminJS offers a feature called "@adminjs/import-export" which solves this use case. You can utilize `csv` , `json` or `xml` files

To install add the package to your project.

```shell
$ yarn add @adminjs/import-export
```

Then add one line (`features`) to resource entry in AdminJS config.

```javascript
import importExportFeature from '@adminjs/import-export';

import componentLoader from './component-loader.js';
...

{
  resource: Entity,
  features: [
    importExportFeature({ componentLoader }),
  ],
}
```

After that you should see two buttons (Import, Export) in the right top corner of  the resource view)

Please remember that field names in imported files should be the same as in database model.


# Password

@adminjs/passwords

The password feature can be utilized to hash a user's password when editing it's record.

Installation

```shell
$ yarn add @adminjs/passwords
```

Next step is to add feature option to user resource

```javascript
import argon2 from 'argon2';
import passwordsFeature from '@adminjs/passwords';

import User from './models/user.js';
import componentLoader from './component-loader.js';

const adminJsOptions = {
  resources: [
    {
      resource: User,
      options: {
        //...your regular options go here'
        properties: { password: { isVisible: false } },
      },
      features: [
        passwordsFeature({
          componentLoader,
          properties: {
            encryptedPassword: 'password',
            password: 'newPassword'
          }
          hash: argon2.hash,
      })
      ]
    },
  ],
  //...
}
```

In the example above `password` is `User` property which holds encrypted password (we have to make it invisible to keep secure. `newPassword` in `passwordsFeature -> options` properties is a virtual field which keeps entered password and will be hashed before saving. &#x20;


# Leaflet Maps

@adminjs/leaflet

The Leaflet feature (`@adminjs/leaflet`) integrates Leaflet (<https://leafletjs.com/>) into AdminJS allowing you to visualize your maps and markers.

## Installation

```bash
$ yarn add @adminjs/leaflet # or: npm install @adminjs/leaflet
```

## Usage

`@adminjs/leaflet` package exports two features that you should use based on your use case.

* `leafletSingleMarkerMapFeature` should be used when your resource is the `Marker` itself, and you only want to display a single marker on the rendered map. This should be used when, for example, you'd like to change a specific marker's location without displaying all your markers on a map.

<figure><img src="/files/1sUrjMI9ZKSw44L2mm4t" alt=""><figcaption><p>leafletSingleMarkerMapFeature</p></figcaption></figure>

* `leafletMultipleMarkersMapFeature` should be used when your resource is a collection of `Marker`s. An example could be a `Map` resource with `Marker` being a separate resource. This feature will allow you to manage all associated markers.

{% hint style="warning" %}
`leafletMultipleMarkersMapFeature` will not display the map when creating a new `Map` record due to dynamic management of markers. Whenever you drag, edit, create or delete a `Marker`, the change takes effect immediately which relies on your `Map` to had been created before.  If you want to manage your `Map` markers, create the `Map` first and you can manage them via `edit` action.
{% endhint %}

<figure><img src="/files/oL8LkCWhBpNbWAUZPD2m" alt=""><figcaption><p>leafletMultipleMarkersMapFeature</p></figcaption></figure>

The usage tutorial will be split into two sections related to the feature you're using but there are some common steps you must take to get the features working.

First of all, you have to import `getLeafletDist` from `@adminjs/leaflet`. Leaflet library has it's own CSS which is required for the maps to display properly but AdminJS core does not let you import CSS files directly into React components (as of version 6). As a workaround, `@adminjs/leaflet` exports `getLeafletDist` utility function which resolves a path to Leaflet dist files in your `node_modules`. These dist files have to be exposed by your server framework. The example below shows how you can do it with `express`.

```typescript
import leafletFeatures, { getLeafletDist } from '@adminjs/leaflet';
import AdminJS, { ComponentLoader } from 'adminjs';
import express from 'express';

// ...

const app = express();
// Use express.static to serve public files
app.use(express.static(getLeafletDist()));

// ...

const componentLoader = new ComponentLoader();
const admin = new AdminJS({
  componentLoader,
  assets: {
    // Tell AdminJS that leaflet.css is available under <server url>/leaflet.css
    styles: ['/leaflet.css'],
  },
});
```

Alternatively, you can provide a CDN link to `leaflet.css` in `styles`.

In the example above, we also instantiated a `ComponentLoader`. Take note of that, as it will be used by Leaflet features later.

### leafletSingleMarkerMapFeature

The `AdminJS` configuration described above should be extended with your `Marker` resource.

The feature goes inside `features` in your resource specification. Take a look at the example below which includes comments on how you can configure the feature.

```typescript
import leafletFeatures, { getLeafletDist } from '@adminjs/leaflet';
// other imports
import Marker from './marker.entity.js';

// Other code - remember to set up the common configuration!

const admin = new AdminJS({
  resources: [
    {
      resource: Marker,
      features: [
        leafletFeatures.leafletSingleMarkerMapFeature({
          /* You must provide your "componentLoader" instance for the feature
          to add it's components. */
          componentLoader,
          /* You must provide "paths" for the feature to know which fields
          contain your marker's coordinates or where to display the map in the UI. */
          paths: {
            /* A property which should be used to display the map. It can be an entirely
            new property name, or you can use an actual field from your Marker's model. */
            mapProperty: 'location',
            /* "jsonProperty" is optional and should only be given if your Marker's latitude
            and longitude are stored in a single JSON field. In this example, "location"
            is of GeoJSON.Point type (https://geojson.org/). If your latitude and longitude
            are stored in separate fields, leave this option undefined. */
            jsonProperty: 'location',
            /* If your latitude has a separate field in your model, just use the field name.
            Example:
              latitudeProperty: 'latitude'
            If your latitude property is a part of a JSON structure (GeoJSON example from above)
            you must provide a flattened path under which the latitude should be saved in the JSON. */
            latitudeProperty: 'location.coordinates.0',
            /* Longitude configuration is the same as latitude's */
            longitudeProperty: 'location.coordinates.1',
            /* 'location.coordinates.0' combined with 'location.coordinates.1' will save the
            coordinates in the following format: { coordinates: [<lat>, <lng>] } */
          },
          /* "baseValue" should be left undefined unless your coordinates field is of JSON structure
          that needs additional constant attributes. The GeoJSON example described above requires
          the Point payload to be: { type: 'Point', coordinates: [<lat>, <lng>] }
          Providing only "paths.latitudeProperty" and "paths.longitudeProperty" will send the coordinates
          as: { coordinates: [<lat>, <lng>] } with `type: 'Point'` missing. "baseValue" can be used
          so that `type: 'Point'` is always added to the payload.
          Please note that "baseValue" is only required for JSON structures which require extra elements.
          If your latitude and longitude are stored in separate fields, please leave "baseValue" undefined. */
          baseValue: { type: 'Point', coordinates: [] },
          /* "mapProps" are passed to React Leaflet's MapContainer component. You can use them to
          change initial zoom, initial coordinates, max zoom, map bounds, disable scroll zoom, etc.
          Reference: https://react-leaflet.js.org/docs/v3/api-map/ */
          mapProps: undefined,
          /* "tileProps" are passed to React Leaflet's TileLayer component. You can use them
          to provide your custom tile URL, attribution, etc.
          Reference: https://react-leaflet.js.org/docs/v3/api-components/#tilelayer */
          tileProps: undefined,
        }),
      ],
    },
  ],
  componentLoader,
  assets: {
    styles: ['/leaflet.css'],
  },
});
```

### leafletMultipleMarkersMapFeature

The `AdminJS` configuration described above should be extended with your `MapEntity` resource.

{% hint style="info" %}
We use `MapEntity` instead of simply `Map` for entity name because `Map` is a reserved name in Javascript.
{% endhint %}

The feature goes inside `features` in your resource specification. Take a look at the example below which includes comments on how you can configure the feature.

```typescript
import leafletFeatures, { getLeafletDist } from '@adminjs/leaflet';
// other imports
import MapEntity from './map.entity.js';

// Other code - remember to set up the common configuration!

const admin = new AdminJS({
  resources: [
    {
      resource: MapEntity,
      features: [
        leafletFeatures.leafletMultipleMarkersMapFeature({
          /* You must provide your "componentLoader" instance for the feature
          to add it's components. */
          componentLoader,
          /* Since Map and Marker are in 1:M relation, a property to display the map
          will not be present by default and the feature has to create one.
          In this example, "mapProperty" creates a new "markers" field in your Map's "edit"
          and "show" views. */
          mapProperty: 'markers',
          /* "markerOptions" are required for the feature to know where to get and how to manage
          your markers. Please note that "edit", "new" and "list" actions have to be enabled in your
          Marker resource. */
          markerOptions: {
            /* Your marker's resource ID. This is usually either the model name or a table name
            of your Marker unless you'd changed it. */
            resourceId: 'Marker',
            /* The foreign key in your Marker model which associates it with currently managed Map */
            foreignKey: 'mapId',
            /* This configuration is exactly the same as "paths" configuration
            in "leafletSingleMarkerMapFeature" */
            paths: {
              /* A property which should be used to display the map. It can be an entirely
              new property name, or you can use an actual field from your Marker's model. */
              mapProperty: 'location',
              /* "jsonProperty" is optional and should only be given if your Marker's latitude
              and longitude are stored in a single JSON field. In this example, "location"
              is of GeoJSON.Point type (https://geojson.org/). If your latitude and longitude
              are stored in separate fields, leave this option undefined. */
              jsonProperty: 'location',
              /* If your latitude has a separate field in your model, just use the field name.
              Example:
                latitudeProperty: 'latitude'
              If your latitude property is a part of a JSON structure (GeoJSON example from above)
              you must provide a flattened path under which the latitude should be saved in the JSON. */
              latitudeProperty: 'location.coordinates.0',
              /* Longitude configuration is the same as latitude's */
              longitudeProperty: 'location.coordinates.1',
              /* 'location.coordinates.0' combined with 'location.coordinates.1' will save the
              coordinates in the following format: { coordinates: [<lat>, <lng>] } */
            },
          },
          /* "mapProps" are passed to React Leaflet's MapContainer component. You can use them to
          change initial zoom, initial coordinates, max zoom, map bounds, disable scroll zoom, etc.
          Reference: https://react-leaflet.js.org/docs/v3/api-map/ */
          mapProps: {
            /* In "leafletSingleMarkerMapFeature" the map is centered at your marker by default.
            In "leafletMultipleMarkersMapFeature" the markers are fetched asynchronously and the map
            is rendered before they're available so you must provide initial coordinates yourself.
            By default, if you don't provide "center", the map will be centered at London coordinates.
            
            Future versions of "@adminjs/leaflet" should have better handling of initial coordinates. */
            center: [52.237049, 21.017532],
          },
          /* "tileProps" are passed to React Leaflet's TileLayer component. You can use them
          to provide your custom tile URL, attribution, etc.
          Reference: https://react-leaflet.js.org/docs/v3/api-components/#tilelayer */
          tileProps: undefined,
        }),
      ],
    },
  ],
  componentLoader,
  assets: {
    styles: ['/leaflet.css'],
  },
});
```

{% hint style="info" %}
`leafletMultipleMarkersMapFeature` works best when combined with `leafletSingleMarkerMapFeature` in your `Marker` resource.
{% endhint %}

## Example

An example application can be found in [@adminjs/leaflet GitHub repository](https://github.com/SoftwareBrothers/adminjs-leaflet/tree/main/example-app).

### Installation

```bash
$ git clone https://github.com/SoftwareBrothers/adminjs-leaflet.git
$ cd adminjs-leaflet
$ yarn install
$ yarn build
$ cd example-app
$ yarn install
$ docker-compose up -d
$ yarn start
```

## Issues & Ideas

`@adminjs/leaflet` is still in its early development phase. If you have any ideas on how to improve it, add new features or if you find any bugs, please create an issue in its [GitHub repository](https://github.com/SoftwareBrothers/adminjs-leaflet/issues).


# Writing your own features

Features simplify writing code that is shared between your resources as they automatically handle merging of configuration.

A simple feature feature could be implemented as follows:

```javascript
const feature = (prevResourceOptions) {
  return {
    ...prevResourceOptions,
    actions: {
      ...prevResourceOptions.actions,
      edit: {
        ...(prevResourceOptions.actions && prevResourceOptions.actions.edit),
        //..
      }
      //..
    }
  }
}

export { feature }
```

As you can see, in the example above you have to take care of merging previous options, which could be problematic. Fortunately AdminJS gives you helper functions which help with this:&#x20;

* a factory function [buildFeature](https://github.com/SoftwareBrothers/adminjs/blob/master/src/backend/utils/build-feature/build-feature.ts),
* optional helper [mergeResourceOptions](https://github.com/SoftwareBrothers/adminjs/blob/master/src/backend/decorators/resource/resource-options.interface.ts) (when you need more control)

&#x20;This is how a feature could look like when [buildFeature](https://github.com/SoftwareBrothers/adminjs/blob/master/src/backend/utils/build-feature/build-feature.ts) is used:

```javascript
import { buildFeature, FeatureType } from 'adminjs';

import SomeModel from 'some-model.entity.js';

const someBeforeHook = () => { /* noop */ };

const myFeature = (config = {}): FeatureType => {
  // do something with your feature config?

  return buildFeature({
    actions: {
      edit: {
        before: [someBeforeHook],
      },
    }
  });
}

const SomeResource: ResourceWithOptions = {
  resource: SomeModel,
  features: [myFeature({})],
};

/* "someBeforeHook" will be used as a "before" hook in "edit" actions
for every resource where "myFeature" is added */
```


# API

There are seven default actions defined for each resource. Each of that actions has an automatically generated REST API endpoint (i.e. `/api/resources/{resourceId}/actions/{action}`)

Available actions are:

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>List</strong></td><td><em>allows you to list and filer all the records for a given resource</em></td><td></td><td><a href="/pages/dsABrFL3D64bUeVvXGlS">/pages/dsABrFL3D64bUeVvXGlS</a></td></tr><tr><td><strong>New</strong></td><td><em>is responsible for creating a new record in a given resource</em></td><td></td><td><a href="/pages/WnKjsZLuONpJKvpUkKmg">/pages/WnKjsZLuONpJKvpUkKmg</a></td></tr><tr><td><strong>Search</strong></td><td><em>allows you to search records in a given resource by a query string</em></td><td></td><td><a href="/pages/kdFqeDk6x1Qqcrjt6yE0">/pages/kdFqeDk6x1Qqcrjt6yE0</a></td></tr><tr><td><strong>Show</strong></td><td><em>is responsible for showing the details of a record</em></td><td></td><td><a href="/pages/UsihpLUdUxQnTB6hWwNR">/pages/UsihpLUdUxQnTB6hWwNR</a></td></tr><tr><td><strong>Edit</strong></td><td><em>is responsible for editing record in a given resource</em></td><td></td><td><a href="/pages/pig8rWmsu94wuvW4VtTZ">/pages/pig8rWmsu94wuvW4VtTZ</a></td></tr><tr><td><strong>Delete</strong></td><td><em>is responsible for deleting single records</em></td><td></td><td><a href="/pages/l5wzNljRa3WaQYmM5i1I">/pages/l5wzNljRa3WaQYmM5i1I</a></td></tr><tr><td>Bulk Delete</td><td><em>is responsible for deleting multiple records</em></td><td></td><td><a href="/pages/y8P3oeXLV2R3189r1C7U">/pages/y8P3oeXLV2R3189r1C7U</a></td></tr></tbody></table>


# List

allows you to list and filer all the records for a given resource

**Endpoint:** `/api/resources/[RESOURCE-ID]/actions/list`

**Method:** GET

**Request params:**&#x20;

* `direction` - sorting direction, possible values `asc`,`desc`
* `sortBy` - name of the sorting column&#x20;
* `page` - requested page number&#x20;
* `perPage` - number of records per page (max `500)`
* `filers.[field_name]` - filters applied&#x20;

**Response:**

* `meta`
  * `total` - total number of records in the resource
  * `perPage` - number of records in a single page
  * `page` - number of requested page
  * `direction`- sorting direction, possible values `asc`,`desc`
  * `sortBy`- id of the sorting column
* `records` - list of records with resource metadata

**Example**&#x20;

[*https://demo.adminjs.com*/admin/api/resources/Admin/actions/list?direction=desc\&sortBy=\_id\&filters.email=admin\&page=1](https://demo.adminjs.com/admin/api/resources/Admin/actions/list?direction=desc\&sortBy=_id\&filters.email=admin\&page=1)

```json
{
   "meta":{
      "total":1,
      "perPage":10,
      "page":1,
      "direction":"desc",
      "sortBy":"_id"
   },
   "records":[
      {
         "params":{
            "_id":"62d50386c2d13cd087a10e3a",
            "email":"admin@example.com",
            "password":"$argon2id$v=19$m=4096,t=3,p=1$PFUAZpgSO1XwfnksafaV2Q$+vJ1hrmDAY70Us5iz5bNttDRCOAxLGIAOFaol0KrcjI",
            "__v":0
         },
         "populated":{
            
         },
         "baseError":null,
         "errors":{
            
         },
         "id":"62d50386c2d13cd087a10e3a",
         "title":"admin@example.com",
         "recordActions":[
            {
               "name":"show",
               "actionType":"record",
               "icon":"Screen",
               "label":"Show",
               "resourceId":"Admin",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"default",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            }
         ],
         "bulkActions":[
            
         ]
      }
   ]
}
```


# New

is responsible for creating a new record in a given resource

**Endpoint:** `/api/resources/[RESOURCE-ID]/actions/new`

**Method:** POST

**Request payload:**&#x20;

* `FormData` object with all required fields for the given resource

**Response:**

* `meta`
  * `total` - total number of records in the resource
  * `perPage` - number of records in a single page
  * `page` - number of requested page
  * `direction`- sorting direction, possible values `asc`,`desc`
  * `sortBy`- id of the sorting column
* `records` - list of records with resource metadata

**Example**&#x20;

Endpoint: [https://demo.adminjs.co/admin/api/resources/User/actions/new](https://adminjs-demo.herokuapp.com/admin/api/resources/User/actions/new)

Payload:&#x20;

<pre class="language-json"><code class="lang-json">{
    email: "client@adminjs.co",
<strong>    firstName: "ClientName",
</strong><strong>    lastName: "ClientSurname",
</strong><strong>    gender: "male",
</strong><strong>    isMyFavourite: true
</strong>}
</code></pre>


# Search

allows you to search records in a given resource by a query string (by default it's the title property)

**Endpoint:** `/api/resources/[RESOURCE-ID]/actions/search/[SEARCH-PHRASE]?[SEARCH-CONDITIONS]`

**Method:** GET

**Request params:**&#x20;

* `title` - searching by title
* `filers.[field_name]` - searching by field values
* `page` - requested page number&#x20;
* `perPage` - number of records per page (max `500)`
* `sortBy` - id of the sorting column&#x20;
* `direction` - sorting direction, possible values `asc`,`desc`

**Response:**

* `records` - list of records with resource metadata
  * `record` - record you're requesting
    * `params` - all record data&#x20;
    * `id` - record id
    * `title` - record title
    * `recordActions`- list all actions and their parameters available on this record
    * `bulkActions`- list of all bulk actions and their parameters available on this record

**Example:**

Endpoint: <https://adminjs-demo.herokuapp.com/admin/api/resources/categories/actions/search/Games>

```json
{
   "records":[
      {
         "params":{
            "id":2,
            "name":"Games",
            "createdAt":"2023-01-30T13:01:39.597Z",
            "updatedAt":"2023-01-30T13:01:39.597Z"
         },
         "populated":{
            
         },
         "baseError":null,
         "errors":{
            
         },
         "id":2,
         "title":"Games",
         "recordActions":[
            {
               "name":"show",
               "actionType":"record",
               "icon":"Screen",
               "label":"Show",
               "resourceId":"categories",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"default",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            },
            {
               "name":"edit",
               "actionType":"record",
               "icon":"Edit",
               "label":"Edit",
               "resourceId":"categories",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"default",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            },
            {
               "name":"delete",
               "actionType":"record",
               "icon":"TrashCan",
               "label":"Delete",
               "resourceId":"categories",
               "guard":"Do you really want to remove this item?",
               "showFilter":false,
               "showResourceActions":true,
               "component":false,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"danger",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            }
         ],
         "bulkActions":[
            {
               "name":"bulkDelete",
               "actionType":"bulk",
               "icon":"Delete",
               "label":"Delete all",
               "resourceId":"categories",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":true,
               "hideActionHeader":false,
               "containerWidth":"500px",
               "layout":null,
               "variant":"danger",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            }
         ]
      }
   ]
}
```


# Show

is responsible for showing the details of a record

**Endpoint:**  `/api/resources/User/records/[RESOURCE-ID]/show`

**Method:** GET

**Response:**

* `record` - record you're requesting
  * `params` - all record data&#x20;
  * `id` - record id
  * `title` - record title
  * `recordActions`- list all actions and their parameters available on this record scoped to the logged in user
  * `bulkActions`- list of all bulk actions and their parameters available on this record scoped to the logged in user

**Example**&#x20;

Endpoint: [https://demo.adminjs.co/admin/api/resources/User/records/63d3b2c982bf27f5606e44eb/show](https://adminjs-demo.herokuapp.com/admin/api/resources/User/records/63d3b2c982bf27f5606e44eb/show)

Response:&#x20;

```json
{
   "record":{
      "params":{
         "_id":"63d3b2c982bf27f5606e44eb",
         "firstName":"Admin Name",
         "lastName":"Admin Surname",
         "gender":"male",
         "email":"admin@adminjs.com",
         "isMyFavourite":true,
         "__v":0
      },
      "populated":{
         
      },
      "baseError":null,
      "errors":{
         
      },
      "id":"63d3b2c982bf27f5606e44eb",
      "title":"admin@adminjs.com",
      "recordActions":[
         {
            "name":"show",
            "actionType":"record",
            "icon":"Screen",
            "label":"Show",
            "resourceId":"User",
            "guard":"",
            "showFilter":false,
            "showResourceActions":true,
            "showInDrawer":false,
            "hideActionHeader":false,
            "containerWidth":1,
            "layout":null,
            "variant":"default",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         },
         {
            "name":"edit",
            "actionType":"record",
            "icon":"Edit",
            "label":"Edit",
            "resourceId":"User",
            "guard":"",
            "showFilter":false,
            "showResourceActions":true,
            "showInDrawer":false,
            "hideActionHeader":false,
            "containerWidth":1,
            "layout":null,
            "variant":"default",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         },
         {
            "name":"delete",
            "actionType":"record",
            "icon":"TrashCan",
            "label":"Delete",
            "resourceId":"User",
            "guard":"Do you really want to remove this item?",
            "showFilter":false,
            "showResourceActions":true,
            "component":false,
            "showInDrawer":false,
            "hideActionHeader":false,
            "containerWidth":1,
            "layout":null,
            "variant":"danger",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         }
      ],
      "bulkActions":[
         {
            "name":"bulkDelete",
            "actionType":"bulk",
            "icon":"Delete",
            "label":"Delete all",
            "resourceId":"User",
            "guard":"",
            "showFilter":false,
            "showResourceActions":true,
            "showInDrawer":true,
            "hideActionHeader":false,
            "containerWidth":"500px",
            "layout":null,
            "variant":"danger",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         }
      ]
   }
}
```


# Edit

is responsible for editing record in a given resource

**Endpoint:** `/api/resources/[RESOURCE-ID]/records/[RECORD-ID]/edit`

**Method:** POST

**Request payload:**&#x20;

* `FormData` Object with all required fields for the given resource

**Response:**

* `redirectUrl` - URL that the user should be directed to after a successful update
* `notice`&#x20;
  * `message` - the message that is later displayed in the dashboard
  * `type` - a type of response, possible values are `success`,`error`,`info`
* `record` - record you're requesting
  * `params` - all record data&#x20;
  * `id` - record id
  * `title` - record title
  * `recordActions`- list all actions and their parameters available on this record
  * `bulkActions`- list of all bulk actions and their parameters available on this record
* `records` - list of records with resource metadata

**Example**&#x20;

Endpoint: [https://demo.adminjs.co/admin/api/resources/User/actions/new](https://adminjs-demo.herokuapp.com/admin/api/resources/User/actions/new)

Payload:&#x20;

<pre class="language-json"><code class="lang-json">{
    email: "client@adminjs.co",
<strong>    firstName: "ClientName",
</strong><strong>    lastName: "ClientSurname",
</strong><strong>    gender: "male",
</strong><strong>    isMyFavourite: true
</strong>}
</code></pre>

Response:

```json
{
   "redirectUrl":"/admin/resources/User",
   "notice":{
      "message":"Successfully updated given record",
      "type":"success"
   },
   "record":{
      "params":{
         "_id":"63d3b2c982bf27f5606e44eb",
         "firstName":"Admin Name",
         "lastName":"Admin Surname",
         "gender":"male",
         "email":"admin1@adminjs.com",
         "isMyFavourite":true,
         "__v":0
      },
      "populated":{
         
      },
      "baseError":null,
      "errors":{
         
      },
      "id":"63d3b2c982bf27f5606e44eb",
      "title":"admin1@adminjs.com",
      "recordActions":[
         {
            "name":"show",
            "actionType":"record",
            "icon":"Screen",
            "label":"Show",
            "resourceId":"User",
            "guard":"",
            "showFilter":false,
            "showResourceActions":true,
            "showInDrawer":false,
            "hideActionHeader":false,
            "containerWidth":1,
            "layout":null,
            "variant":"default",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         },
         {
            "name":"edit",
            "actionType":"record",
            "icon":"Edit",
            "label":"Edit",
            "resourceId":"User",
            "guard":"",
            "showFilter":false,
            "showResourceActions":true,
            "showInDrawer":false,
            "hideActionHeader":false,
            "containerWidth":1,
            "layout":null,
            "variant":"default",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         },
         {
            "name":"delete",
            "actionType":"record",
            "icon":"TrashCan",
            "label":"Delete",
            "resourceId":"User",
            "guard":"Do you really want to remove this item?",
            "showFilter":false,
            "showResourceActions":true,
            "component":false,
            "showInDrawer":false,
            "hideActionHeader":false,
            "containerWidth":1,
            "layout":null,
            "variant":"danger",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         }
      ],
      "bulkActions":[
         {
            "name":"bulkDelete",
            "actionType":"bulk",
            "icon":"Delete",
            "label":"Delete all",
            "resourceId":"User",
            "guard":"",
            "showFilter":false,
            "showResourceActions":true,
            "showInDrawer":true,
            "hideActionHeader":false,
            "containerWidth":"500px",
            "layout":null,
            "variant":"danger",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         }
      ]
   }
}
```


# Delete

is responsible for deleting single records

**Endpoint:** `/api/resources/[RESOURCE-ID]/records/[RECORD-ID]/delete`

**Method:** GET

**Response**

* `record` - record you're requesting
  * `params` - all record data&#x20;
  * `id` - record id
  * `title` - record title
  * `recordActions`- list all actions and their parameters available on this record
  * `bulkActions`- list of all bulk actions and their parameters available on this record

**Example**&#x20;

Endpoint: [https://demo.adminjs.co/admin/api/resources/User/records/63d3b2c982bf27f5606e44eb/delete](https://adminjs-demo.herokuapp.com/admin/api/resources/User/records/63d3b2c982bf27f5606e44eb/delete)

Response:&#x20;

```json
{
   "record":{
      "params":{
         "_id":"63d3b2c982bf27f5606e44eb",
         "firstName":"Admin Name",
         "lastName":"Admin Surname",
         "gender":"male",
         "email":"admin@adminjs.com",
         "isMyFavourite":true,
         "__v":0
      },
      "populated":{
         
      },
      "baseError":null,
      "errors":{
         
      },
      "id":"63d3b2c982bf27f5606e44eb",
      "title":"admin@adminjs.com",
      "recordActions":[
         {
            "name":"show",
            "actionType":"record",
            "icon":"Screen",
            "label":"Show",
            "resourceId":"User",
            "guard":"",
            "showFilter":false,
            "showResourceActions":true,
            "showInDrawer":false,
            "hideActionHeader":false,
            "containerWidth":1,
            "layout":null,
            "variant":"default",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         },
         {
            "name":"edit",
            "actionType":"record",
            "icon":"Edit",
            "label":"Edit",
            "resourceId":"User",
            "guard":"",
            "showFilter":false,
            "showResourceActions":true,
            "showInDrawer":false,
            "hideActionHeader":false,
            "containerWidth":1,
            "layout":null,
            "variant":"default",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         },
         {
            "name":"delete",
            "actionType":"record",
            "icon":"TrashCan",
            "label":"Delete",
            "resourceId":"User",
            "guard":"Do you really want to remove this item?",
            "showFilter":false,
            "showResourceActions":true,
            "component":false,
            "showInDrawer":false,
            "hideActionHeader":false,
            "containerWidth":1,
            "layout":null,
            "variant":"danger",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         }
      ],
      "bulkActions":[
         {
            "name":"bulkDelete",
            "actionType":"bulk",
            "icon":"Delete",
            "label":"Delete all",
            "resourceId":"User",
            "guard":"",
            "showFilter":false,
            "showResourceActions":true,
            "showInDrawer":true,
            "hideActionHeader":false,
            "containerWidth":"500px",
            "layout":null,
            "variant":"danger",
            "parent":null,
            "hasHandler":true,
            "custom":{
               
            }
         }
      ]
   }
}
```


# Bulk Delete

is responsible for deleting multiple records

**Endpoint:** `/api/resources/[RESOURCE-ID]/bulk/bulkDelete`

**Method:** POST

**Request:**

* `recordIds` - list of ids of the records to be deleted&#x20;

**Response:**

* `records` - list of records with resource metadata
* `record` - record you're requesting
  * `params` - all record data&#x20;
  * `id` - record id
  * `title` - record title
  * `recordActions`- list all actions and their parameters available on this record
  * `bulkActions`- list of all bulk actions and their parameters available on this record
* `notice`&#x20;
  * `message` - the message that is later displayed in the dashboard
  * `type` - a type of response, possible values are `success`,`danger`,`info`
* `redirectUrl` - URL that the user should be directed to after successfully edit

**Example**&#x20;

Endpoint: <https://demo.adminjs.co/admin/api/resources/User/bulk/bulkDelete?recordIds=63d3af2ab1b453f9303c81d0&recordIds=63d3af2ab1b453f9303c81d1>

Response:&#x20;

```json
{
   "records":[
      {
         "params":{
            "_id":"63d3af2ab1b453f9303c81d0",
            "firstName":"Warren",
            "lastName":"Renner",
            "gender":"female",
            "email":"Naomi90@gmail.com",
            "isMyFavourite":true,
            "__v":0
         },
         "populated":{
            
         },
         "baseError":null,
         "errors":{
            
         },
         "id":"63d3af2ab1b453f9303c81d0",
         "title":"Naomi90@gmail.com",
         "recordActions":[
            {
               "name":"show",
               "actionType":"record",
               "icon":"Screen",
               "label":"Show",
               "resourceId":"User",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"default",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            },
            {
               "name":"edit",
               "actionType":"record",
               "icon":"Edit",
               "label":"Edit",
               "resourceId":"User",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"default",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            },
            {
               "name":"delete",
               "actionType":"record",
               "icon":"TrashCan",
               "label":"Delete",
               "resourceId":"User",
               "guard":"Do you really want to remove this item?",
               "showFilter":false,
               "showResourceActions":true,
               "component":false,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"danger",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            }
         ],
         "bulkActions":[
            {
               "name":"bulkDelete",
               "actionType":"bulk",
               "icon":"Delete",
               "label":"Delete all",
               "resourceId":"User",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":true,
               "hideActionHeader":false,
               "containerWidth":"500px",
               "layout":null,
               "variant":"danger",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            }
         ]
      },
      {
         "params":{
            "_id":"63d3af2ab1b453f9303c81d1",
            "firstName":"Jovanny",
            "lastName":"Moore",
            "gender":"male",
            "email":"Vanessa53@hotmail.com",
            "isMyFavourite":false,
            "__v":0
         },
         "populated":{
            
         },
         "baseError":null,
         "errors":{
            
         },
         "id":"63d3af2ab1b453f9303c81d1",
         "title":"Vanessa53@hotmail.com",
         "recordActions":[
            {
               "name":"show",
               "actionType":"record",
               "icon":"Screen",
               "label":"Show",
               "resourceId":"User",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"default",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            },
            {
               "name":"edit",
               "actionType":"record",
               "icon":"Edit",
               "label":"Edit",
               "resourceId":"User",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"default",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            },
            {
               "name":"delete",
               "actionType":"record",
               "icon":"TrashCan",
               "label":"Delete",
               "resourceId":"User",
               "guard":"Do you really want to remove this item?",
               "showFilter":false,
               "showResourceActions":true,
               "component":false,
               "showInDrawer":false,
               "hideActionHeader":false,
               "containerWidth":1,
               "layout":null,
               "variant":"danger",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            }
         ],
         "bulkActions":[
            {
               "name":"bulkDelete",
               "actionType":"bulk",
               "icon":"Delete",
               "label":"Delete all",
               "resourceId":"User",
               "guard":"",
               "showFilter":false,
               "showResourceActions":true,
               "showInDrawer":true,
               "hideActionHeader":false,
               "containerWidth":"500px",
               "layout":null,
               "variant":"danger",
               "parent":null,
               "hasHandler":true,
               "custom":{
                  
               }
            }
         ]
      }
   ],
   "notice":{
      "message":"successfully removed 2 record",
      "type":"success"
   },
   "redirectUrl":"/admin/resources/User"
}
```


# Themes

@adminjs/themes

{% hint style="info" %}
Themes are only available in AdminJS version 7 or higher.
{% endhint %}

Themes are a new feature of AdminJS version 7 which introduces new UI customization options for developers.

#### Features

* You can provide a custom configuration (<https://styled-system.com/theme-specification/>) per theme, allowing you to easily modify the general look of your admin panel.
* You can provide a custom `style.css` file per theme if you need to.
* You can override any AdminJS default component per theme. Please note that if you override the same component yourself, your own component will take precedence and will be used across all your themes.
* You can assign different themes to specific users based on their role or whatever logic you wish to use. This is done by setting `theme` in `currentAdmin` object.

## Installation & Usage

### Installation

```bash
$ yarn add @adminjs/themes
```

### Usage

`@adminjs/themes` repository contains public themes which you can easily import into your application. It also provides a CLI which you can use to easily generate and bundle your themes.

#### Importing public themes

AdminJS version 7 comes with two new options you can use when instantiating it. These are `availableThemes` and `defaultTheme`.

* `defaultTheme` is an identifier of a theme which will be used as a default one if your `currentAdmin` does not have `theme` defined.
* `availableThemes` is a list of themes which are available in your application.

If you choose not to provide `defaultTheme` and `availableThemes`, the admin panel will behave the same as before version 7 where you can modify the admin panel's look using `branding.theme`.

```typescript
import { dark, light, noSidebar } from '@adminjs/themes'
import AdminJS from 'adminjs'

// ...

const admin = new AdminJS({
  defaultTheme: dark.id,
  availableThemes: [dark, light, noSidebar],
})
```

In the example above we import three public themes from `@adminjs/themes`

* `dark` which is a dark theme for AdminJS
* `light` which is a light theme for AdminJS (basically, the default theme)
* `noSidebar` is a theme which uses components overrides to remove the sidebar and instead moves the resources to the top bar.

Please note that by following the example above all users that sign into your admin panel will see the `dark` theme. An example of how you can use different themes for different users will be shown later.

#### Importing custom themes

Every theme that you import from `@adminjs/themes` is actually a configuration object in the following format:

```typescript
type ThemeConfig = {
  id: string,                          # Theme ID, example: "my-custom-theme"
  name: string,                        # Example: "My Custom Theme"
  overrides: Partial<ThemeOverride>;   # "styled-system" theme configuration
  bundlePath?: string;                 # Path to your theme's "theme.bundle.js" file
  stylePath?: string;                  # Path to your theme's "style.css" file
}
```

With this knowledge, you can create your own custom themes. Please take a look at the example below where we define a simple custom theme which changes the primary color of the admin panel to `teal`.

```typescript
import AdminJS from 'adminjs'

// ...

const myCustomTheme = {
  id: 'my-custom-theme',
  name: 'My Custom Theme',
  overrides: {
    colors: {
      primary100: 'teal',
    },
  },
}
/* We're leaving "bundlePath" and "stylePath" undefined
since we don't use a css file nor custom components. */

// ...

const admin = new AdminJS({
  defaultTheme: myCustomTheme.id,
  availableThemes: [myCustomTheme],
})
```

#### Custom themes with components overrides

To learn how to bundle components for your themes, refer to CLI section below. This section in turn covers how you should configure your theme to know where to search for your bundle file.

If your theme has custom components, then you must define `bundlePath` in it's configuration and/or `stylePath` if it also has it's `style.css` file. The example below is an extension of what was shown previously.

```typescript
import path from 'path';
import * as url from 'url';

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

// ...

const myCustomTheme = {
  id: 'my-custom-theme',
  name: 'My Custom Theme',
  overrides: {
    colors: {
      primary100: 'teal',
    },
  },
  bundlePath: `${path.join(__dirname, `../themes/my-custom-theme`)}/theme.bundle.js`,
  stylePath: `${path.join(__dirname, `../themes/my-custom-theme`)}/style.css`,
}

// *
```

To learn more about how to override components in your themes, take a look at how `no-sidebar` theme is implemented in [@adminjs/themes repository](https://github.com/SoftwareBrothers/adminjs-themes/tree/main/src/themes/no-sidebar/components).

All theme-specific components that are theme's overriden components must be placed in theme's `components` directory. Component's file name must match the name of the core component you are overriding. In `no-sidebar`'s theme case, these components are `Sidebar` and `TopBar`.

A full list of overridable components' names can be found in [the core repository](https://github.com/SoftwareBrothers/adminjs/blob/feat/adminjs-v7/src/frontend/utils/overridable-component.ts). If you wish to, you can even override `Application` component and write the entire UI from the scratch.

### Assigning themes to specific users

How you assign a theme to a user is up to the developer working on AdminJS panel. You can create a `theme` field in your users collection, you can also assign themes dynamically when i. e. authenticating. Below you can find an example of how this can be done:

```typescript
import { UserRepository } from './user.repository.js'

/* "authenticate" is an authentication function, please refer to "Plugins"
section to see how to set up an authenticated admin panel. */
const authenticate = (email, password) => {
  /* An example of email/password authentication */
  const userRepository = new UserRepository()
  const user = await userRepository.findByEmail(email)
  
  if (!user) return null
  
  if (await !user.comparePassword(password)) return null
  
  const currentAdmin = {
    id: user.id,
    email: user.email,
    role: user.role,
  }

  /* Assigning themes based on role */
  if (currentAdmin.role === 'Admin') {
    // "Admin" has "dark" theme
    currentAdmin.theme = 'dark'
  } else {
    // Any other role has "light" theme
    currentAdmin.theme = 'light'
  }
  
  return currentAdmin
}
```

## CLI

`@adminjs/themes` comes with a CLI tool which can help you develop your themes.

### Commands

```bash
$ npx adminjs-themes generate <options>
$ npx adminjs-themes bundle <options>
```

#### #generate

`generate` command creates an empty theme (with no modifications) which is prepared to be imported into your AdminJS instance.

**Example**

&#x20;`npx adminjs-themes generate "My Custom Theme"`

**Arguments**

* Name of your theme which will be used as it's `id` in kebab-case format (`My Custom Theme` becomes `my-custom-theme`). It is also used as `name` by default if `--description` is not provided.

**Options**

* `--description` `[string]` - a parameter which sets `name` in your theme configuration.
* `--output` `[string]` - the output directory where the theme will be generated. Defaults to `./src/themes`

#### #bundle

`bundle` command bundles your theme's custom components into `theme.bundle.js` file. Please note that this file should be commited into your source code.

**Example**

`npx adminjs-themes bundle "my-custom-theme"`

**Arguments**

* The ID of the theme you want to bundle. If you do not provide the ID, all themes in your input directory will be bundled.

**Options**

* `--input` `[string]` - the input directory with your AdminJS themes. Default to `./src/themes`. In most cases it should match `--output` from `generate` command.

## Contributing

If you would like to help develop `@adminjs/themes` library, please visit it's [repository](https://github.com/SoftwareBrothers/adminjs-themes).

If you want to share your theme with others, open a pull request where you commit it into `src/themes` directory. Make sure that your pull request contains all relevant files and that it does not modify other themes. Videos or pictures of your theme are welcome.

Make sure to update the `example` inside of the repository.

## Examples

An example usage of themes can be found in it's [repository](https://github.com/SoftwareBrothers/adminjs-themes/tree/main/example).

Themes are also included in our [demo application](https://demo.adminjs.co/).

You can see the themes by signing in using the following credentials schema:

```
Email: <THEME_ID>@example.com
Password: password
```

Example:

```
Email: dark@example.com
Password: password
```

The source code of the demo application can be found in it's [repository](https://github.com/SoftwareBrothers/adminjs-example-app).


# Authentication

Version 7.4 of `adminjs` package introduces authentication providers which both simplify and extend authentication possibilities in your application.

Every authentication provider extends `BaseAuthProvider` class (exported by `adminjs` package). `adminjs` also exports `DefaultAuthProvider` which functions exactly the same as the current `authenticate` method.

### DefaultAuthProvider

As of version >=7.4.0 of `adminjs`, `DefaultAuthProvider` is an alternative to `authenticate` method. In the next major release, `authenticate` method will be removed in favour of auth providers.

<pre class="language-typescript"><code class="lang-typescript"><strong>import { DefaultAuthProvider } from 'adminjs';
</strong><strong>
</strong><strong>import componentLoader from '&#x3C;path to your component loader>';
</strong><strong>
</strong><strong>// Placeholder authentication function, add your logic for authenticating users
</strong>const authenticate = ({ email, password }, ctx) => {
  return { email };
}

const authProvider = new DefaultAuthProvider({
  componentLoader,
  authenticate,
});

// ...

// Express example, in other plugins the change is exactly the same
// "provider" should be configured at the same level as "authenticate" previously
  const router = buildAuthenticatedRouter(
    admin,
    {
      // "authenticate" was here
      cookiePassword: 'test',
      provider: authProvider,
    },
    null,
    {
      secret: 'test',
      resave: false,
      saveUninitialized: true,
    }
  );
</code></pre>

By migrating to class syntax, you should be able to modify any existing auth provider without making additional changes to your framework's plugin.

### BaseAuthProvider

```typescript
export interface LoginHandlerOptions {
  data: Record<string, any>;
  query?: Record<string, any>;
  params?: Record<string, any>;
  headers: Record<string, any>;
}

export interface RefreshTokenHandlerOptions extends LoginHandlerOptions {}

export class BaseAuthProvider {
  public getUiProps(): Record<string, any> {
    return {}
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  public async handleLogin(opts: LoginHandlerOptions, context?: any) {
    throw new NotImplementedError('BaseAuthProvider#handleLogin')
  }

  public async handleLogout(context?: any): Promise<any> {
    return Promise.resolve()
  }

  public async handleRefreshToken(opts: RefreshTokenHandlerOptions, context?: any): Promise<any> {
    return Promise.resolve({})
  }
}
```

You can use `getUiProps` method to define configuration that will be sent to the frontend.

`handleLogin` method is neccessary to sign in your user and should return a user object or `null`, for example:

```typescript
  override async handleLogin(opts: LoginHandlerOptions, context) {
    const { data = {} } = opts
    const { email, password } = data

    return this.authenticate({ email, password }, context)
  }
```

`context` of `handleLogin` will always be an object containing Request/Response objects specific to your framework of choice.

`handleLogout` and `handleRefreshToken` are optional. `handleLogout` will be called before your user's session is destroyed in case you have to perform additional actions to log out the user. `handleRefreshToken` can be used to refresh your user's session if it's matched with an external authentication service. `handleRefreshToken` should return an updated user object (i. e. with a new access token). It is not used by default, but you can override `AuthenticationBackgroundComponent` component to periodically refresh your session.

```typescript
import { useCurrentAdmin } from 'adminjs';

const api = new ApiClient();

// ...

const AuthenticationBackgroundComponentOverride = () => {
  const [currentAdmin, setCurrentAdmin] = useCurrentAdmin();
  // ...
  // A part of your code responsible for refreshing user's session
  const requestBody = {};
  const response = await api.refreshToken(requestBody);

  const { data } = response;

  setCurrentAdmin(data);
  // ...
  return null;
}

export default AuthenticationBackgroundComponentOverride;
```


# FirebaseAuthProvider

@adminjs/firebase-auth

`@adminjs/firebase-auth` is an authentication provider which allows you to sign in using Firebase Authentication.

<figure><img src="/files/TvjbQkA8syNS6DeEkhwr" alt=""><figcaption></figcaption></figure>

## Prerequisites

Make sure you follow official Firebase documentation to properly set up Firebase Authentication in Firebase Console: <https://firebase.google.com/docs/auth>

## Installation

`@adminjs/firebase-auth` is a premium feature which can be purchased at <https://cloud.adminjs.co>

All premium features currently use **One Time Payment** model and you can use them in all apps that belong to you. Once you purchase the addon, you will receive a license key which you should provide in `@adminjs/firebase-auth` configuration in your application's code.

Installing the library:

```bash
$ yarn add @adminjs/firebase-auth
```

The license key should be provided via `FirebaseAuthProvider` constructor:

```typescript
new FirebaseAuthProvider({
  licenseKey: process.env.LICENSE_KEY,
  // the rest of the config
})
```

If you encounter any issues or require help installing the package please contact us through our Discord server.

## Usage

`FirebaseAuthProvider` requires you to prepare Firebase-specific configuration.

#### UI Config

`@adminjs/firebase-auth` uses `firebaseui-web` to generate Firebase UI inside the login form and it requires you to configure it. Please refer to the following link for configuration options: <https://github.com/firebase/firebaseui-web/blob/master/types/index.d.ts#L115>

Note that you can only configure raw values but you will not be able to configure functions or callbacks this way. A workaround will be described in the later part of this documentation.

```typescript
import { EmailAuthProvider } from 'firebase/auth';

const uiConfig = {
  popupMode: true,
  signInFlow: 'popup',
  signInOptions: [
    {
      provider: EmailAuthProvider.PROVIDER_ID,
      disableSignUp: {
        status: true,
      },
    },
  ],
};
```

#### Firebase Configuration

`@adminjs/firebase-auth` initializes a Firebase App in the Login view. Make sure you copy the configuration from your project in Firebase Console.

```typescript
const firebaseConfig = {
  apiKey: 'AIza...',
  authDomain: 'XXXX.firebaseapp.com',
  projectId: 'XXXX',
  storageBucket: 'XXXX.appspot.com',
  messagingSenderId: '11111111111',
  appId: '1:11111111111:web:abcdef',
};
```

You will most likely also have to initialize a Firebase app on your server's end to verify the user's access token later:

```typescript
import { initializeApp } from 'firebase-admin/app';

// ...

const firebaseApp = initializeApp(firebaseConfig);
```

#### Authenticate method

Lastly, you must define an `authenticate` method which you will use to verify the user's access token and return the user object.

```typescript
import { getAuth } from 'firebase-admin/auth';
import { FirebaseAuthenticatePayload } from '@adminjs/firebase-auth';

export const authenticate = async ({
  accessToken,
}: FirebaseAuthenticatePayload) => {
  const auth = getAuth(firebaseApp);

  try {
    const decodedToken = await auth.verifyIdToken(accessToken);

    return {
      id: decodedToken.uid,
      email: decodedToken.email ?? '',
      avatarUrl: decodedToken.picture,
    };
  } catch (error) {
    console.log(error);
    return null;
  }
};
```

### FirebaseAuthProvider Configuration

After you are done with Firebase-specific configuration, you can instantiate `FirebaseAuthProvider`:

```typescript
import { FirebaseAuthProvider } from '@adminjs/firebase-auth';
import componentLoader from '<path to your component loader file>';

// ... assume Firebase related configuration is in the same file

const authProvider = new FirebaseAuthProvider({
  // make sure that the same ComponentLoader instance is configured in AdminJS!
  componentLoader,
  uiConfig,
  firebaseConfig,
  authenticate,
  licenseKey: process.env.LICENSE_KEY,
});

// ...

// Add "provider" to authentication options of your framework plugin, Express example:

const router = buildAuthenticatedRouter(
  admin,
  {
    cookiePassword: 'test',
    provider: authProvider,
  },
  null,
  {
    secret: 'test',
    resave: false,
    saveUninitialized: true,
  }
);
```

With your plugin and auth provider configured you should be able to restart your server and a new login page with embedded Firebase UI should appear.

## Troubleshooting

#### How to prevent @adminjs/firebase-auth from overriding the Login page?

By default, `@adminjs/firebase-auth` will override your Login page with it's own UI. You can disable that by adding `overrideLogin: false` in `FirebaseAuthProvider` configuration:

```typescript
const authProvider = new FirebaseAuthProvider({
  componentLoader,
  uiConfig,
  firebaseConfig,
  authenticate,
  licenseKey: process.env.LICENSE_KEY,
  overrideLogin: false,
});
```

If you do this, though, Firebase won't render it's own UI. You will have to create your own Login page and import `FirebaseAuthForm` component from `@adminjs/firebase-auth` and put it wherever you want in your own Login component.

```typescript
import { FirebaseAuthForm } from '@adminjs/firebase-auth/components'

const CustomLogin = () => {
 return <FirebaseAuthForm />
}

export default CustomLogin
```

Remember to override `Login` using your component loader:

```typescript
componentLoader.override('Login', '<path to custom login>');
```

#### How to configure functions and callbacks of UI Config?

Follow the steps above to create your custom Login page. Create UI configuration in your custom component and provide it as props of `FirebaseAuthForm`:

```typescript
const uiConfig = {/* custom config */};

const CustomLogin = () => {
 return <FirebaseAuthForm uiConfig={uiConfig} />
}
```

#### How to customize the look of Firebase UI?

If you decide to follow the steps above you will be able to create a styled component out of `FirebaseAuthForm` and customize it fully.

Alternatively, you can create a CSS file and add it to your app's assets.

```css
.adminjs_firebaseui-container {
  background: red;
}
```

Make sure the CSS file is present in your server's public assets directory. Lastly, configure `assets` of AdminJS:

```typescript
const admin = new AdminJS({
  assets: {
    styles: ['/firebase-ui.css'],
  },
  // other config
})
```


# MatrixAuthProvider

Matrix User Authentication for the @adminjs/matrix plugin.

## Authentication - Matrix User Authentication

[➡️ Plugin - @adminjs/matrix](/installation/plugins/matrix)

***

### Matrix User Authentication

Authenticate users directly against your Matrix server using the `MatrixAuthProvider`.

#### Setup Example

```typescript
import { MatrixAuthProvider } from '@adminjs/matrix';
import componentLoader from './component-loader.js';

const provider = new MatrixAuthProvider({
  baseUrl: process.env.MATRIX_BASE_URL,
  componentLoader,
});

export default provider;
```

#### Notes

* Users will be authenticated directly with their Matrix username and password.
* This method does **not** use a shared token – authentication is user-specific.
* You must configure the `MATRIX_BASE_URL` environment variable correctly for your Matrix server.

***

### Related

* [Plugin Setup - @adminjs/matrix](/installation/plugins/matrix)


# How to write an addon?

With the introduction of AdminJS Marketplace, members of the AdminJS community can now publish their created plugins there, either for free or for a fee. In this article, we will describe how you can write your own plugin and what steps to take to make it appear in the AdminJS Marketplace at <https://cloud.adminjs.co>.

## Writing addons

There are various categories of addons that you can develop, but most fall into the following classifications:

1. **Adapters:** These are libraries that establish a connection between your AdminJS panel and a database, either directly or through an ORM/ODM.
2. **Plugins:** These libraries enable you to configure an AdminJS panel using different frameworks, facilitating the setup of API endpoints and views.
3. **Features:** Libraries categorized as features extend resource configuration to offer built-in functionalities, such as upload logic or generic actions.
4. **Themes:** Themes can range from a simple recoloring of the default AdminJS panel to a complete overhaul of its visual appearance.
5. **Authentication Providers:** Introduced in version 7.4.0 of the `adminjs` package, authentication providers simplify and broaden the possibilities for authenticating into AdminJS.

If you're keen on participating in the development of AdminJS, we recommend reviewing the following articles within this documentation:

{% content-ref url="/pages/IiSGqfUDPbQa9pMLZSmz" %}
[Resource](/basics/resource)
{% endcontent-ref %}

{% content-ref url="/pages/BJXXvuorHfcWEamuW2od" %}
[Action](/basics/action)
{% endcontent-ref %}

{% content-ref url="/pages/BGOUMQIuwNJEkgSaB3JQ" %}
[Property](/basics/property)
{% endcontent-ref %}

{% content-ref url="/pages/TDPQKQ6yDS9Tom1nf5oo" %}
[Writing your own features](/basics/features/writing-your-own-features)
{% endcontent-ref %}

{% content-ref url="/pages/vnHaxjeCl9izYWBhxuIp" %}
[Writing your own Components](/ui-customization/writing-your-own-components)
{% endcontent-ref %}

{% content-ref url="/pages/KMAbrrHN9Fcx0T6xSxjj" %}
[Themes](/basics/themes)
{% endcontent-ref %}

{% content-ref url="/pages/uMDZVF713oTIp2oZ73Xn" %}
[Authentication](/basics/authentication)
{% endcontent-ref %}

In case the articles above are not sufficient, feel free to reach out to us via our [official Discord server](https://adminjs.page.link/discord), and we will be happy to assist you.

## Guidelines

As addons are published on official AdminJS channels, there are several guidelines that each addon must adhere to in order to be published on the Marketplace:

1. Usage of Typescript to maintain the readability of the code,
2. Usage of ESLint to keep the code clean,
3. Compatibility with the latest versions of AdminJS packages and building ESM output,

Every submitted addons will also have to be reviewed and tested by an AdminJS developer.

## Submitting your addons

To publish your addon, please contact us through the [contact form](https://share.hsforms.com/1IedvmEz6RH2orhcL6g2UHA8oc5a) or [Discord](https://adminjs.page.link/discord). In the future, the process of adding addons will be automated.


# Writing your own Components

## Adding custom components

AdminJS handles all properties and actions using its own, built-in components. These are the familiar text inputs, checkboxes, paginated lists, edit forms etc. However, you can define a custom component for any single property or action instead. This gives you great control over how the data is being displayed and modified.

In general, you need to create an instance of `ComponentLoader`, add your custom components there, pass it in the AdminJS options object and specify which properties/actions are using your custom components and where.

`./components.ts`:

```typescript
import { ComponentLoader } from 'adminjs'

const componentLoader = new ComponentLoader()

const Components = {
    MyInput: componentLoader.add('MyInput', './my-input'),
    // other custom components
}

export { componentLoader, Components }
```

`./my-input.tsx`

```tsx
import React from 'react'

// just some regular React component
const MyInputComponent = () => <input />

export default MyInputComponent
```

`./some-resource.ts`

```typescript
import { Components } from './components.js'

export const SomeResource = {
  resource: Something, // database model
  options: {
    properties: {
      someText: {
        type: 'string',
        components: {
          edit: Components.MyInput, // this is our custom component
        },
      },
    },
  },
}
```

`./index.ts`

<pre class="language-typescript"><code class="lang-typescript">import { AdminJS } from 'adminjs'
import { componentLoader } from './components.js'
import { SomeResource } from './some-resource.js'

const admin = new AdminJS({
    resources: [SomeResource],
    componentLoader, // the loader needs to be added here
    // other options
})

admin.watch() // this builds your frontend code in development environment

<strong>// rest of the adapter and plugin code
</strong></code></pre>

Components for actions are added in the exact same way, only in the action section of a resource instead of properties. Refer to [Action](/basics/action#action-with-custom-component) and [Property](/basics/property#creating-custom-properties) pages for more information.

### Custom Component Structure

Files added to `ComponentLoader` need to expose the component function as a default export. Both `.jsx` and `.tsx` formats are supported.

**Advanced usage:** The path passed in the second argument to the `ComponentLoader.add()` and `ComponentLoader.override()` functions needs to be relative to the file where it was called. If you want to wrap these calls in another function you might find that AdminJS cannot find the correct files. To fix that, pass a third argument with the name of the caller function, like this:

```typescript
import { ComponentLoader } from 'adminjs'

const loader = new ComponentLoader()

export const bundleFile = (key: string, path: string) => {
    loader.add(key, path, 'bundleFile') // `bundleFile` is the name of this function
}
```

### Dependencies

AdminJS uses these dependencies internally, so they are exposed for your code without the need to require them in the `package.json` file:

* [react](https://reactjs.org/)
* [react-dom](https://reactjs.org/)
* [prop-types](https://github.com/facebook/prop-types)

**State management**

* [redux](https://redux.js.org/)
* [react-redux](https://github.com/reduxjs/react-redux)

**Routing**

* [react-router](https://reacttraining.com/react-router/)
* [react-router-dom](https://reacttraining.com/react-router/)

**Styling**

* [styled-components](https://www.styled-components.com/docs)
* [styled-system](https://www.styled-system.com/)

**Other**

* [axios](https://github.com/axios/axios)
* [flat](https://www.npmjs.com/package/flat)
* [react-feather](https://feathericons.com/)

### Props passed to components

In your property and action components, you can use props passed by their *controlling components*.

Currently we have 2 *controlling components*:

* one for an action: [BaseActionComponent](https://adminjs.page.link/base-action) with [ActionProps](https://github.com/SoftwareBrothers/adminjs/blob/master/src/frontend/components/actions/action.props.ts)
* and one for custom property field: [BasePropertyComponent](https://github.com/SoftwareBrothers/adminjs/blob/master/src/frontend/components/actions/action.props.ts) with [BasePropertyProps](https://adminjs.page.link/base-property-props)

Check out their documentation to see available **props**

> Other, internal components (like Dashboard) have either no or different props, see the source code in each case.

## Overriding internal AdminJS components

`ComponentLoader` also has an `.override()` method that lets you replace components used by AdminJS internally by your own custom components. This is useful in cases where you want to change or add behavior to the entire AdminJS app, for example:

* Adding a custom dashboard
* Changing the app layout
* Overriding entire controls (like replacing boolean checkboxes with toggles)
* Customizing component look and feel when theming is insufficient

Overriding component works exactly the same way as adding custom components, but you need to specify the matching name of the component ([here's the list](https://github.com/SoftwareBrothers/adminjs/blob/master/src/frontend/utils/overridable-component.ts)).

The methods are split into `.add()` and `.override()` as a safety layer, so you don't accidentally override an internal component with a custom component of the same name - the functions will throw an error when used for conflicting components. In short, `.add()` won't let you use internal component names and `.override()` requires an internal component name.

## Other customizations

### Theming

We support [Theme](https://adminjs.page.link/theme) compatible with <https://system-ui.com/theme> standard.

In order to override default colors, fonts, sizes etc., you can put your values in AdminJSOptions.branding.

#### Using style props

AdminJS components are supercharged with multiple props controlling styles. For instance in order to change color of a module:@adminjs/design-system.Button you can pass *backgroundColor* (bg) from the module:@adminjs/design-system.Theme like that:

```html
<Button bg="primary60"></Button>
```

For all possible options visit the [Theme](https://docs.adminjs.co/Theme.html) description.

#### Adding custom css to components

If using style props is not enough - you can always pass your custom CSS. So for instance let's assume that you would like to overwrite CSS in a Button component. You can do this like that:

```typescript
import { Button } from '@adminjs/design-system'

const MyButton = styled(Button)`
  background-color: #ccc;
  color: ${({theme}) => theme.colors.grey100};
  ...
`
```

We use [styled-components](https://styled-components.com/) under the hood so make sure to check out their docs.

### Reusing UI Components of AdminJS

AdminJS gives you the ability to reuse its component library:

```typescript
import { Label } from '@adminjs/design-system'

const YourComponent (props) => {(
  <Label>Some styled text<Label>
)}
```

> We divide components internally to 2 groups:
>
> * *application components* - which requires AdminJS, you can think about them as "smart components"
> * and *design system components* - they don't require AdminJS and you can use them outside of the AdminJS setup.
>
> That is why sometimes you have to import components from 'adminjs' package and sometimes from '@adminjs/design-system'.

Each of the components is described with the playground option, so make sure to check out all the documentation of all the components.

One of the most versatile component is a [BasePropertyComponent](https://adminjs.page.link/base-property-code). It allows you to render any property. Combined with [useRecord](https://adminjs.page.link/use-record) is a powerful tool for building forms.

### Creating Custom Pages

You can also use custom components as full pages by specifying their name in the `pages` object in AdminJS options:

```typescript
import AdminJS from 'adminjs'
import { Components } from './components.js'

new AdminJS({
    pages: {
        myPage: { // name, will be used to build an URL
            handler: // handler code,
            component: Components.MyPage,
            icon: // page icon name
        }
    }
})
```

### Using other AdminJS frontend classes and objects

AdminJS also exposes following classes:

* [ApiClient](https://adminjs.page.link/api-client)
* [ViewHelpers](https://adminjs.page.link/view-helpers)

You can use them like this:

```typescript
import { ApiClient, ViewHelpers } from 'adminjs'
```


# Overwriting CSS styles

Admin comes with default look. As of version 6.4 there is an easy way to change default styles. We added special `data-css` attributes to essential html tags. Their values are build dynamically and depend on resource, action and container.

#### Simple example

If we have the resource `user` we can style entire `form` as well as individual fields in this form. In this specific case `form` has `data-css="users-edit-form"` (*edit* is an action name). Field `password` is tagged by `users-edit-password`&#x20;

This naming convention give you ability to style every resource, every action and every container separately. You can  also use more general attribute selector: for instance `[data-css$="edit-form"]` to style every form in AdminJS

#### Configuration

First, you must assure that AdminJS has access to static files. For express based app you should add line similar to this:

```typescript
import * as url from 'url'
// other imports

const __dirname = url.fileURLToPath(new URL('.', import.meta.url))

app.use(express.static(path.join(__dirname, "../public")));
```

Next, you should tell AdminJS where your CSS style sheet file is located, placing following code in your config file (where `sidebar.css` is name your CSS file and its location is in `public` ):

```javascript
assets: {
    styles: ["/sidebar.css"],
}
```

#### Ready to use example

Below you find simple example changing colors in AdminJS sidebar

```css
:root {
  --topbar-color: white;
  --sidebar-bg-color: darkgray;
  --sidebar-color: white;
  --sidebar-link-color: orange;
}

section[data-css="sidebar"] {
  background-color: var(--sidebar-bg-color) !important;
  color: var(--sidebar-color);
  border: none;
}

section[data-css="sidebar"] svg {
  fill: var(--sidebar-color) !important;
}

a[data-css="sidebar-logo"] {
  background-color: var(--sidebar-bg-color) !important;
}

section[data-css="sidebar-resources"] {
  background: var(--sidebar-bg-color) !important;
}

[data-css="sidebar"] section a {
  background: var(--sidebar-bg-color) !important;
  color: var(--sidebar-color);
}

[data-css="sidebar"] a:hover {
  color: var(--sidebar-link-color);
}
```


# Dashboard customization

By default, AdminJS comes with a simple dashboard which you may want to customize in most cases.

<figure><img src="/files/G9sDaQ1XjVtUDcDrIDHF" alt=""><figcaption><p>Default Dashboard</p></figcaption></figure>

The customization of the dashboard consists of two required and one optional steps:

1. Creating a React component for your dashboard
2. Configuring the dashboard to use your component
3. (Optional) Creating a `handler` for your dashboard to access server data

## Creating a React component for your dashboard

This step requires you to create a React component which is pretty much straightforward. Please refer to the [source code of default dashboard](https://github.com/SoftwareBrothers/adminjs/blob/master/src/frontend/components/app/default-dashboard.tsx) to get started.

`useTranslation` hook can be imported from `adminjs` library:

```typescript
import { useTranslation } from 'adminjs'
```

The rest of the code can be copied as is and worked on.

## Configuring the dashboard to use your component

Now you will have to tell AdminJS to use your dashboard. This can be achieved by configuring the `dashboard` option when instantiating `AdminJS`. Please refer to ["Writing your own Components"](/ui-customization/writing-your-own-components) tutorial to find out how you can bundle your dashboard component.

Assuming that this is your `ComponentLoader`:

```typescript
import { ComponentLoader } from 'adminjs'

const componentLoader = new ComponentLoader()

const Components = {
  Dashboard: componentLoader.add('Dashboard', './dashboard'),
  // other custom components
}
```

This would be how you override the dashboard:

```typescript
const admin = new AdminJS({
  dashboard: {
    component: Components.Dashboard,
  },
  componentLoader
})
```

Restart your server and you should see your new, customized dashboard.

## (Optional) Creating a handler for your dashboard to access server data

In some cases you might want to access backend data in your dashboard, for example when you would like to display charts or statistics in general. To do this, you have to create a `handler` for you dashboard.

```typescript
const dashboardHandler = async () => {
  // Asynchronous code where you, e. g. fetch data from your database
  
  return { message: 'Hello World' }
}
```

The handler your create has to be assigned to `dashboard` option in your `AdminJS` instance:

```typescript
const admin = new AdminJS({
  dashboard: {
    component: Components.Dashboard,
    handler: dashboardHandler,
  },
  componentLoader
})
```

You now have the logic for returning the data to the frontend, the last part is accessing it in your dashboard component. To do this, you can use AdminJS's `ApiClient` in your React component:

```tsx
import { ApiClient } from 'adminjs'
import React, { useEffect, useState } from 'react'

// ...
const [data, setData] = useState(null)
const api = new ApiClient()

useEffect(() => {
  api.getDashboard()
    .then((response) => {
      setData(response.data) // { message: 'Hello World' }
    })
    .catch((error) => {
      // handle any errors
    });
}, [])

// ...

console.log(data.message) // "Hello World"
```

Save your component, restart your server and you're good to go.


# Changing the form view

Default form view is based on CSS flex property with  `flex-direction: column`

If you would like to change this view AdminJS deliver layout option for actions (`new`, `edit` and `view`).

For example:

If our model has following fields: `name`, `surname`, `login`, `password` and we would like to have name and surname in one row and login and password below (also in one row) for new action we can use following example to achieve this

```javascript
    actions: [
      {
        name: 'new',
        layout: [
          ['@Header', { children: 'Enter user details' }],
          [
            { flexDirection: 'row', flex: true },
            [
              ['name', { flexGrow: 1, marginRight: '10px' }],
              ['surname', { flexGrow: 1 }],
            ],
          ],
          ['@Header', { children: 'Enter user credentials' }],
          [
            { flexDirection: 'row', flex: true },
            [
              ['login', { flexGrow: 1, marginRight: '10px' }],
              ['password', { flexGrow: 1 }],
            ],
          ],

        ],
      },
      // other actions
    ],
```

More detail information about layout structure you can find [here](https://github.com/SoftwareBrothers/adminjs/blob/master/src/backend/utils/layout-element-parser/layout-element.doc.md)


# Role-Based Access Control

Role-based access control allows your application to limit access to resources, records and actions only to specific users. This is not a feature in AdminJS, but rather a way of configuring it to your needs using your own, custom code.

## Setup

Your app should be set up with a [plugin](/installation/plugins) and an [adapter](/installation/adapters) with the authenticated router. This will give us access to the user object throughout the AdminJS config file. Let's assume your user object will look similar to this TypeORM model:

```typescript
@Entity({ name: 'users' })
class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  public id!: number;

  @Column()
  public email!: string;

  @Column()
  public role!: string;

  @Column()
  public password!: string;
}
```

When setting up an authenticated router, there's an async `authenticate` function in the config. It receives email and password, and should return the matching user object like the one above (or `null` if credentials are invalid).

## Managing users

All passwords should be hashed in the database and never exposed to the outside world. AdminJS however doesn't know which fields of which models should be treated as such, so you need to manually remove passwords from the responses.

This is done using `after` and `before` hooks - you can inspect the request and response objects and modify them before or after they're handled in AdminJS by removing or hashing all passwords. Note that `after` hook in the `edit` action is called twice - first time as `GET` to get the data for editing (we need to clear passwords there) and again as `POST` to update the data (here we need to hash the new password). Also, `list` action returns a *list* of records, so we need to iterate over them to remove passwords. This makes most action hooks a little bit different.

Lastly, we can hide password fields from views that don't need to display them. This is done using `isVisible` setting in properties. Please remember that this only hides the UI elements, it doesn't prevent AdminJS from sending the property, so we still need those `after` hooks.

```typescript
const userResource: ResourceWithOptions = {
  resource: User,
  options: {
    actions: {
      new: {
        before: async (request) => {
          if (request.payload?.password) {
            request.payload.password = hash(request.payload.password);
          }
          return request;
        },
      },
      show: {
        after: async (response: RecordActionResponse) => {
          response.record.params.password = '';
          return response;
        },
      },
      edit: {
        before: async (request) => {
          // no need to hash on GET requests, we'll remove passwords there anyway
          if (request.method === 'post') {
            // hash only if password is present, delete otherwise
            // so we don't overwrite it
            if (request.payload?.password) {
              request.payload.password = hash(request.payload.password);
            } else {
              delete request.payload?.password;
            }
          }
          return request;
        },
        after: async (response: RecordActionResponse) => {
          response.record.params.password = '';
          return response;
        },
      },
      list: {
        after: async (response: ListActionResponse) => {
          response.records.forEach((record) => {
            record.params.password = '';
          });
          return response;
        },
      },
    },
    properties: {
      password: {
        isVisible: {
          list: false,
          filter: false,
          show: false,
          edit: true, // we only show it in the edit view
        },
      },
    },
  },
};
```

You can find more information about adding logic to your properties in the Property Logic tutorial.

## Restricting access to actions

To remove access to a whole action you can use `isAccessible` setting. This will remove the UI elements and block all API requests for those specific actions.

```typescript
const someResource: ResourceWithOptions = {
  resource: Something,
  options: {
    actions: {
      new: {
        isAccessible: false,
      },
    },
  },
};
```

You can also pass it a function that decides whether currently performed action is accessible depending on context like current user:

```typescript
const someResource: ResourceWithOptions = {
  resource: Something,
  options: {
    actions: {
      new: {
        isAccessible: ({ currentAdmin }) => currentAdmin.role === 'admin',
      },
    },
  },
};
```

Or restrict access to specific actions based on the content of the record. All this applies to custom actions as well.

```typescript
const someResource: ResourceWithOptions = {
  resource: Something,
  options: {
    actions: {
      publish: {
        isAccessible: ({ record }) => !record.params.published,s
        // rest of the custom action code
      },
    },
  },
};
```

### Difference between `isAccessible` and `isVisible`

Both of those settings hide specific actions from the UI, but `isAccessible` also blocks API calls to this action. If you have custom components where you want to programatically call API endpoints for specific action, but don't want to display it to the user, you can use `isVisible` to hide it instead. This is often useful for custom actions that have some additional logic behind triggering them.

## Restricting access to specific properties

By default, AdminJS displays and allows editing of all properties of an object. You can control that to a degree using options `listProperties`, `editProperties` etc, but this hides the UI for all users. Hiding these properties based on user's role is a little bit more involved.

In general, you want to capture the `resource` object before it's rendered by an action component, and remove all properties that shouldn't be displayed for this specific role. You do this by creating a custom action component that adjusts the data and passes it back into the default action component.

```typescript
import React, { FC } from 'react';
import {
  ActionProps,
  BaseActionComponent,
  BasePropertyJSON,
  useCurrentAdmin,
} from 'adminjs';

const CustomAction: FC<ActionProps> = (props) => {
  const [currentAdmin] = useCurrentAdmin();
  const newProps = { ...props };
  
  // This is important - `component` option controls which custom
  // component is rendered by `BaseActionComponent` and we don't
  // want to render this code here again. That would create an
  // infinite loop.
  newProps.action = { ...newProps.action, component: undefined };

  // Configuration is stored in each property's custom props.
  const filter = (property: BasePropertyJSON) => {
    const { role } = property.custom;
    return !role || currentAdmin?.role === String(role);
  };

  // Since we want to remove properties from all actions, a common
  // filtering function can be used.
  const { resource } = newProps;
  resource.listProperties = resource.listProperties.filter(filter);
  resource.editProperties = resource.editProperties.filter(filter);
  resource.showProperties = resource.showProperties.filter(filter);
  resource.filterProperties = resource.filterProperties.filter(filter);

  // `BaseActionComponent` will now render the default action component
  return <BaseActionComponent {...newProps} />;
};

export default CustomAction;
```

The code above will hide the UI elements on the frontend, but the responses from the API will still contain all data that was hidden. Editing hidden data with tools like Postman will also be possible. You may want to patch that up using action hooks. Here's an example of an `after` hook that is generalized for every action:

```typescript
const roleAccessControlAfterHook = async (
  response: any,
  _: any,
  context: ActionContext,
) => {
  const { properties } = context.resource
    .decorate()
    .toJSON(context.currentAdmin);
  const targetRole = context.currentAdmin?.role;
  const propertiesToRemove = Object.entries(properties)
    .filter(
      ([_, { custom }]) => custom.role && String(custom.role) !== targetRole,
    )
    .map(([name]) => name);

  const cleanupRecord = (record: RecordJSON) => {
    propertiesToRemove.forEach((name) => delete record.params[name]);
  };
  if (response.record) {
    cleanupRecord(response.record);
  }
  if (response.records) {
    response.records.forEach(cleanupRecord);
  }
  return response;
};
```

Similar thing can be implemented for `POST` request in `before` hooks in `edit` and `new` actions in order to prevent editing those fields.

```typescript
const roleAccessControlBeforeHook: Before = async (request, context) => {
  const { method, payload } = request;
  if (method !== 'post' || !payload) {
    return request;
  }
  const { properties } = context.resource
    .decorate()
    .toJSON(context.currentAdmin);
  const targetRole = context.currentAdmin?.role;
  const propertiesToRemove = Object.entries(properties)
    .filter(
      ([_, { custom }]) => custom.role && String(custom.role) !== targetRole,
    )
    .map(([name]) => name);
  propertiesToRemove.forEach((name) => delete payload[name]);
  return request;
};
```

In case of the `new` action you might want to add additional `before` hook that would set a default value for the fields you're restricting if they are required in the database.

```typescript
const defaultValuesBeforeHook: Before = async (request, context) => {
  const { payload, method } = request;
  if (method !== 'post' || !payload || context.action.name !== 'new') {
    return request;
  }
  const { properties } = context.resource
    .decorate()
    .toJSON(context.currentAdmin);
  Object.entries(properties).forEach(([name, { custom }]) => {
    if (custom.defaultValue && payload[name] === undefined) {
      payload[name] = custom.defaultValue;
    }
  });
  return request;
};
```

If you need to reuse this functionality in other resources, it might be a good idea to pack it into a feature:

```typescript
const roleBasedAccessControl = buildFeature((admin) => {
  const CustomAction = admin.componentLoader.add(
    'CustomAction',
    './custom-action',
  );
  return {
    actions: {
      new: {
        component: CustomAction,
        before: [roleAccessControlBeforeHook, defaultValuesBeforeHook],
        after: [roleAccessControlAfterHook],
      },
      edit: {
        component: CustomAction,
        before: [roleAccessControlBeforeHook],
        after: [roleAccessControlAfterHook],
      },
      show: {
        component: CustomAction,
        after: [roleAccessControlAfterHook],
      },
      list: {
        component: CustomAction,
        after: [roleAccessControlAfterHook],
      },
    },
  };
});
```

Finally, this configuration will hide the properties specified in the action config from users without a specific role:

```typescript
const someResource: ResourceWithOptions = {
  resource: Something,
  features: [roleBasedAccessControl],
  options: {
    properties: {
      superSecretAdminProperty: {
        custom: {
          role: 'admin',
          defaultValue: 'a secret',
        },
      },
    },
  },
};
```


# Internationalization (i18n)

AdminJS has the default set of texts prepared in some languages. But nothing stands in the way for you to change each of them or even translate AdminJS to a different language.

### Locale option and basic translations

All the translations can be overridden by using [AdminJSOptions#locale](https://adminjs.page.link/options-interface) property.

You can enable translation for all languages built in AdminJS by adding `locale` object to AdminJS options

```typescript
import { locales as AdminJSLocales } from 'adminjs'
// ...
const options = { 
  locale: { 
    language: 'pl', // default language of application (also fallback)
    availableLanguages: Object.keys(AdminJSLocales), 
  }
}
// ...
const adminJs = new AdminJS(options)
```

AdminJS options extend i18n configuration and allow to configure external params like:

| option             | default | description                                                                             |
| ------------------ | ------- | --------------------------------------------------------------------------------------- |
| language           | en      | main language of application                                                            |
| availableLanguages | \['en'] | array of supported langages keys                                                        |
| localeDetection    | false   | enables locale detections <https://github.com/i18next/i18next-browser-languagedetector> |
| withBackend        | false   | enables backend loaded translations <https://github.com/i18next/i18next-http-backend>   |
| translations       | -       | allows adding or override language translations as `Record<langKey, Locale>`            |

To set the default language you can use language property

```javascript
locale: { 
  language: 'pl', 
  availableLanguages: ['en', 'pl'], 
}
```

If you would like to constrain available languages to specific you have to modify `availableLanguages` array

```javascript
locale: { 
  language: 'pl', 
  availableLanguages: ['en', 'pl'], 
  localeDetection: true, 
}
```

AdminJS translations can be managed by backend services using [i18next-http-backend](https://github.com/i18next/i18next-http-backend) package. To enable this option set `withBackend` variable to `true`

```javascript
locale: { 
  language: 'pl', 
  availableLanguages: ['en', 'pl'], 
  localeDetection: true, 
}
```

Default translations can be extended or changed by passing `language` translations object into `locale` config object. Below is a simple example:

```javascript
locale: { 
  language: 'pl', 
  availableLanguages: ['en', 'pl'], 
  localeDetection: true, 
  translations: { 
    pl: { 
      messages: { 
        welcomeOnBoard_title: 'Nowy tyluł pulpitu', 
      }, 
    }, 
    en: { 
      messages: { 
        welcomeOnBoard_title: 'New dashboard title', 
      }, 
    }, 
  }, 
},
```

### Translations groups

All the translation keys are divided into the following groups:

* **actions** - translations for all [actions](https://adminjs.page.link/action-interface-code) - both default actions, and those created by you.
* **buttons** - translations for all kinds of buttons.
* **messages** - translations for all messages in the app
* **labels** - translations for all labels - usually one word. Labels are used to translate resource names.
* **properties** - translations for all properties.
* **pages** - (new in version 7) translations for your pages labels
* **components** - (new in version 7) a section which allows you to scope translations to components

All of them can be specified globally or for a specific resource.

### More detailed example

Let's assume that you want to translate your admin panel into Polish.

This is what it could look like:

> Take a closer look at this example because it contains a different edge cases like translating the `add new item` button for a particular property, or translating labels for your database enums.

```javascript
locale: {
    language: 'pl',
    availableLanguages: ['en', 'pl'],
    localeDetection: true,
    translations: {
      pl: {
        messages: {
          welcomeOnBoard_title: 'Nowy tyluł pulpitu',
        },
        actions: {
          new: 'Stwórz nowy',
          edit: 'Edytuj',
          show: 'Detale',
        },
        buttons: {
          save: 'zapisz',
          // We use i18next with its pluralization logic.
          confirmRemovalMany_1: 'Potwierdź usunięcie {{count}} rekordu',
          confirmRemovalMany_2: 'Potwierdź usunięcie {{count}} rekordów',
        },
        properties: {
          // labels of properties in all resources with name "name"
          // will be translated to "Nazwa".
          name: 'Nazwa',
          nested: 'Zagniezdzone',
          // this is how nested properties (for nested schemas) can be provided
          'nested.width': 'Szerokość',
          // translate values of boolean property
          'isAdmin.true': 'admin',
          'isAdmin.false': 'normalny',
          // translate values of enums:
          'companySize.small': 'mała',
          'companySize.medium': 'średnia',
          'companySize.big': 'duza',
          // tags is an array and we translate button for this array:
          'tags.addNewItem': 'Dodaj nowy tag',
        },
        labels: {
          // here we translate the name of a resource.
          Comment: 'Komentarze',
        },
        resources: {
          Comment: {
            properties: {
              // this will override the name only for Comment resource.
              name: 'Tytuł',
            },
          },
        },
      },
    },
  },
```

### Using i18n in your application and in AdminJS

In the case that you use i18next in your app already, you need to initialize AdminJS in your i18next init callback. In this way AdminJS will add new translations to existing ones:

```typescript
const loadAdminJS = () => {
  const { adminJs, adminRouter } = admin()
  app.use(adminJs.options.rootPath, adminMiddleware, adminRouter)
  app.use('/admin', adminMiddleware, adminController)
}

i18next.init({...}, (err, t) => {
  loadAdminJS()
})
```

### How to use translations in my custom actions/components

Custom components can also be translated as described (add link) Custom component internationalization

### Overriding language detection

Sometimes it might be useful to override language detection. You can do this by setting lang`u`age value to '`cimode`'.

You can also set `locale` value based on `currentAdmin` object (which holds logged user details)

```javascript
locale: (currentAdmin) => currentAdmin.email === 'specific user email' ? { language: 'cimode' } : locale
```

### More options...

On the backend, we use <https://www.i18next.com/> library. So make sure to check out their docs to read more about all the possible options.

Also, you can always check the default English translation file available [in our repo here](https://github.com/SoftwareBrothers/adminjs/blob/v2.0/src/locale/en.ts).


# Content Management System

By default, AdminJS is equipped with a [TipTap](https://tiptap.dev/) editor, which makes it a perfect tool for a Content Management System

#### AdminJS as a Content Management System

To add TipTap to the AdminJS setup you need to change the type of the property holding your content to the `richtext`.

```typescript
const admin = new AdminJS({
    resources: [
      {
        resource: Posts,
        options: {
          properties: {
            postContent: {
              type: 'richtext',
            }
          }
        }
      }
    ]
  })  
```


# Custom components library

@adminjs/custom-components

If standard library components are not enough there is available library with custom ones. It is  growing continuously to fulfill various users needs.

```shell
$ yarn add @adminjs/custom-components
```

Usage is similar to using [own components](/ui-customization/writing-your-own-components) instead writing own component just import one from library.

`./components.ts`

```typescript
import { ComponentLoader } from 'adminjs'
import bundle from '@adminjs/custom-components'

const componentLoader = new ComponentLoader()

const Components = {
  // other custom components
  // 'CustomComponent' is the component name from library
  CustomComponent: bundle(componentLoader, 'CustomComponent'),
}

export { componentLoader, Components }
```


# Custom component internationalization

AdminJS can be used in multiple languages as described in this [tutorial](/tutorials/internationalization-i18n). Creating an international version of our custom components is also possible.

Assume we have created a custom React component as described [here](/ui-customization/writing-your-own-components). We can utilize a generic AdminJS hook called useTranslations.

<pre class="language-javascript"><code class="lang-javascript"><strong>// ... 
</strong>import { useTranslations } from "adminjs"
// ...

const CustomComponent = (props) => {
    const {translateComponent} = useTranslations()
    return &#x3C;div> {translateComponent('CustomComponent.textToTranslate')} &#x3C;/div>
}
</code></pre>

We should also add translations to the component's namespace in our locale config to make this work.

```javascript
const options = {
  // ...
  locale: {
    translations: {
      en: {
        components: {
          CustomComponent: {
            textToTranslate: 'This is text to translate'
          }
        }
      }
    }
  }
  // ...
}
```

If we would like to create custom messages to be handled by useNotice hook we have to add our component section to the messages namespace.

```javascript
const options = {
  // ...
  locale: {
    translations: {
      en: {
        messages: {
          CustomComponent: {
            componentMessage: 'This is a message from a custom component'
          }
        }
      }
    }
  }
  // ...
}
```

Usage:

```javascript
import { useNotice } from "adminjs"
// ...
const sendNotice = useNotice()
// ...
sendNotice({
  message: 'CustomComponent.componentMessage',
  type: 'error',
})
```


# PDF Generator

For physical applications of AdminJS a pdf generator can come in handy, either for generating shipping labels or work orders.

In order to provide PDF generating capabilities within your AdminJS instance you will have to take advantage of the custom components feature and the [jsPDF library](https://github.com/parallax/jsPDF).

First, create a new instance of the Component Loader, and create a Components object that will hold the custom components.

{% code title="index.ts" %}

```typescript
const componentLoader = new ComponentLoader()

const Components = {
    PDFGenerator: componentLoader.add('GeneratePDF', './pdfgenerator.component')
}
```

{% endcode %}

{% hint style="warning" %}
Remember to [pass the Component Loader instance into your AdminJS config](https://docs.adminjs.co/ui-customization/writing-your-own-components#:~:text=componentLoader%2C%20//%20the%20loader%20needs%20to%20be%20added%20here)!
{% endhint %}

Then, we will have to append the PDF generator to a resource and add a handler for the record passed within context.

{% code title="order.resource.ts" %}

```typescript
const orderResource = {
    resource: Order,
    options: {
        actions: {
            PDFGenerator: {
                actionType: 'record',
                icon: 'GeneratePdf',
                component: Components.PDFGenerator,
                handler: (request, response, context) => {
                    const { record, currentAdmin } = context
                    return {
                        record: record.toJSON(currentAdmin),
                        url: pdfgenerator(record.toJSON(currentAdmin))
                    }
                }
            }
        }
    }
}
```

{% endcode %}

With that ready, we can focus on the PDF generator function.

{% code title="pdfgenerator.ts" %}

```typescript
import { RecordJSON } from 'adminjs'
import { jsPDF } from 'jspdf'

const pdfGenerator = (record: RecordJSON): string => {
  const { params } = record
  const doc = new jsPDF()
  
  doc.text(params.orderNum, 10, 10) // example database column called orderNum
  doc.text(params.shippingAddress, 150, 10) // example database column called shippingAddress
  
  const filename = `/${params.id}.pdf`
  doc.save(`./pdfs${filename}`)
  
  return filename
}

export default pdfGenerator
```

{% endcode %}

Now that our PDFs can be created from the record passed within context, we will need to create a place where the pdfs will be stored. Create a folder called 'pdfs', which we will make public through express.

```
.
├── index.ts
├── order.resource.ts
├── pdfHandler.ts
├── pdfgenerator.component.tsx
├── pdfgenerator.ts
└── pdfs
    └── ...
```

{% hint style="warning" %}
Make sure you set the static path before building the router!
{% endhint %}

{% code title="index.ts" %}

```typescript
import path from 'path'
import * as url from 'url'
// other imports

const __dirname = url.fileURLToPath(new URL('.', import.meta.url))

// ...

app.use(express.static(path.join(__dirname, 'pdfs/')))
```

{% endcode %}

Last, but not least, we need a way to open the freshly generated PDF file. Since we've already defined the custom component we're going to stick to the same naming scheme.

{% code title="pdfgenerator.component.tsx" %}

```tsx
import React, { useEffect } from 'react'
import { ApiClient, ActionProps } from 'adminjs'
import { Loader } from '@adminjs/design-system'

const GeneratePdf: React.FC<ActionProps> = (props) => {
  const { record, resource } = props
  const api = new ApiClient()

  useEffect(() => {
    api.recordAction({
      recordId: record.id,
      resourceId: resource.id,
      actionName: 'PDFGenerator'
    }).then((response) => {
      window.location.href = response.data.url
    }).catch((err) => {
      console.error(err)
    })
  }, [])

  return <Loader />
}

export default GeneratePdf
```

{% endcode %}


# Charts

A simple way to implement charts with your AdminJS instance is to use Recharts library and Admin's API.

In order to provide custom components on your dashboard you will have to override the dashboard entirely. The [dashboard customization](/ui-customization/dashboard-customization) article will come in handy.

In your dashboard handler, you will need to fetch and format the required data for charts. For purposes of this guide, we will be using [Recharts](https://recharts.org/), which requires data to be passed in an array of objects.

{% code title="Recharts data variable" %}

```typescript
[
    {
        name: xAxisVariable0,
        value: yAxisVariable0
    },
    {
        name: xAxisVariable1,
        value: yAxisVariable1
    },
    // ...
    {
        name: xAxisVariableN,
        value: yAxisVariableN
    },
]
```

{% endcode %}

Let's assume our database contains a table called movies, which contains three columns - 'title', 'year', and 'score'.

{% code title="dashboard.handler.ts" %}

```tsx
import { Filter } from 'adminjs'

export const dashboardHandler = async (request, response, context) => {
  // finding resource called movies
  const resource = context._admin.findResource('movies')
  // creating new filter, so that we can see only movies released in 2020
  const filter = new Filter({}, resource)
  // finding all records that match provided filter
  const resourceData = await resource.find(filter, { sort: { sortBy: 'year', direction: 'desc' } }, context)
  
  const data = resourceData.map((item) => item.toJSON(context.currentAdmin))
  
  return data
}
```

{% endcode %}

However, currently, the data variable is an array that contains all the records in the following manner.

{% code title="data variable" %}

```typescript
[
{
    params: {
        id: 2104,
        title: 'Arrival',
        year: 2018,
        score: 84
    },
    baseError: null,
    // ...
},
{
    params: {
        id: 2226,
        title: 'Harry Potter and the Goblet of Fire',
        year: 2018,
        score: 96
    },
    baseError: null,
    // ...
},
// ...
]
```

{% endcode %}

We will need to parse to Recharts data format.

{% code title="dashboard.handler.ts" %}

```typescript
// ...
  const years = Array.from(new Set(data?.map((item) => item.params.year))) // Set leaves only unique values, but we need an Array
  const chartdata = years.map(year => { // for every year that we've got
    const scoreArr = data?.filter(filterItem => filterItem.params.year === year) // find movies from a certain year
                          .map(mapItem => mapItem.params.score) // create an array of all the scores from that year
    return (
      {
        name: year,
        score: scoreArr.reduce((a, b) => a + b, 0) / scoreArr.length // create average valuefrom the score array
      })
  })
  return chartdata
```

{% endcode %}

The next step is creating a chart component we will later on put into our dashboard.

{% code title="linechart.component.tsx" %}

```typescript
import type React from 'react'

import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'

export const LineChartComponent: React.FC = ({ data }) => {
  return (
        <LineChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5
          }}
        >
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Line type="monotone" dataKey="score" stroke="#8884d8" activeDot={{ r: 8 }} />
        </LineChart>
  )
}

```

{% endcode %}

The last part is adding the line chart component to our dashboard.

{% code title="dashboard.tsx" %}

```tsx
import { ApiClient } from 'adminjs'
import React, { useState, useEffect } from 'react'

import { LineChartComponent } from './linechart.component.js'

const Dashboard: React.FC = () => {
  const [data, setData] = useState(null)
  const api = new ApiClient()

  useEffect(() => {
    api.getDashboard()
      .then((response) => {
        setData(response.data)
      })
      .catch((error) => {
        // Handle errors here
      })
  }, [])
  return (
    <LineChartComponent data={data} />
  )
}

export default Dashboard
```

{% endcode %}


# Forgot Password

AdminJS does not currently provide an out-of-the-box solution for password recovery. You will need to implement your own way of resetting the password.

In order to add a custom Forgot Password link to Admin's login page you will have to, first, override the login within your start function and provide another custom component that will serve as a login page.

```typescript
admin.overrideLogin({ component: Login })
```

The next step is to create a login page. Here is the default page with the forgot password hyperlink pointing to a  '/forgot-password' address.

{% code title="login.tsx" %}

```typescript
import React from 'react'
import styled, { createGlobalStyle } from 'styled-components'
import { useSelector } from 'react-redux'
import {
  Box,
  H5,
  H2,
  Label,
  Illustration,
  Input,
  FormGroup,
  Button,
  Text,
  MessageBox,
  MadeWithLove,
  themeGet,
} from '@adminjs/design-system'
import { useTranslation, ReduxState } from 'adminjs'

const GlobalStyle = createGlobalStyle`
  html, body, #app {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
  }
`

const Wrapper = styled(Box)`
  align-items: center;
  justify-content: center;
  flex-direction: column;
  height: 100%;
`

const StyledLogo = styled.img`
  max-width: 200px;
  margin: ${themeGet('space', 'md')} 0;
`

export type LoginProps = {
  message?: string
  action: string
}

export const Login: React.FC<LoginProps> = (props) => {
  const { action, message } = props
  const { translateLabel, translateButton, translateProperty, translateMessage } = useTranslation()
  const branding = useSelector((state: ReduxState) => state.branding)

  return (
    <>
      <GlobalStyle />
      <Wrapper flex variant="grey">
        <Box bg="white" height="440px" flex boxShadow="login" width={[1, 2 / 3, 'auto']}>
          <Box
            bg="primary100"
            color="white"
            p="x3"
            width="380px"
            flexGrow={0}
            display={['none', 'none', 'block']}
            position="relative"
          >
            <H2 fontWeight="lighter">{translateLabel('loginWelcome')}</H2>
            <Text fontWeight="lighter" mt="default">
              {translateMessage('loginWelcome')}
            </Text>
            <Text textAlign="center" p="xxl">
              <Box display="inline" mr="default">
                <Illustration variant="Planet" width={82} height={91} />
              </Box>
              <Box display="inline">
                <Illustration variant="Astronaut" width={82} height={91} />
              </Box>
              <Box display="inline" position="relative" top="-20px">
                <Illustration variant="FlagInCog" width={82} height={91} />
              </Box>
            </Text>
          </Box>
          <Box as="form" action={action} method="POST" p="x3" flexGrow={1} width={['100%', '100%', '480px']}>
            <H5 marginBottom="xxl">
              {branding.logo ? <StyledLogo src={branding.logo} alt={branding.companyName} /> : branding.companyName}
            </H5>
            {message && (
              <MessageBox
                my="lg"
                message={message.split(' ').length > 1 ? message : translateMessage(message)}
                variant="danger"
              />
            )}
            <FormGroup>
              <Label required>{translateProperty('email')}</Label>
              <Input name="email" placeholder={translateProperty('email')} />
            </FormGroup>
            <FormGroup>
              <Label required>{translateProperty('password')}</Label>
              <Input
                type="password"
                name="password"
                placeholder={translateProperty('password')}
                autoComplete="new-password"
              />
            </FormGroup>
            <Text mt="xl" textAlign="center">
              <Button variant="primary">{translateButton('login')}</Button>
            </Text>
            <Text mt="lg" textAlign="center">
              {translateMessage('forgotPasswordQuestion')}{' '}
              <a href="/forgot-password">{translateMessage('forgotPassword')}</a>
            </Text>
          </Box>
        </Box>
        {branding.withMadeWithLove ? (
          <Box mt="xxl">
            <MadeWithLove />
          </Box>
        ) : null}
      </Wrapper>
    </>
  )
}

export default Login

```

{% endcode %}

Keep in mind that to keep the custom login page responsive, you will need to provide locale translation for the custom items we have added.

```typescript
const localeEn = {
  language: 'en',
  translations: {
    messages: {
      forgotPasswordQuestion: 'Trouble logging in?',
      forgotPassword: 'Forgot password'
    }
  }
}

export default localeEn

```

Now, depending on your authorization implementation, you will have to handle the request from the 'Forgot password' hyperlink pointing to the '/forgot-password' address.


# Overview

AdminJS Cloud Hosting is a new service which allows you to easily deploy and host your admin panels on AdminJS servers. While still under development, the service is already functional and allows you to host your applications.

The dashboard for managing your applications can be accessed at <https://cloud.adminjs.co>

{% hint style="info" %}
Cloud Hosting dashboard is based on AdminJS itself.
{% endhint %}

To contact us for more details or request the service, please see the [Pricing](https://adminjs.co/pricing) page.

{% hint style="success" %}
AdminJS library still remains open-source and free!
{% endhint %}

Once you request the Cloud Hosting service and gain access to the dashboard, there are three steps you must take to proceed:

1. Create an application. The application will be reviewed by our team, once it is approved you can proceed to step two.
2. Generate your API Key & API Secret pair. You can treat the API Key as your application's "login" and the API Secret as it's "password". You can view your API Key anytime but if you lose your API Secret you must generate a new API Key/Secret pair.
3. Deploy your application.


# Create an Application

Once you request an account and gain access to Cloud Hosting dashboard, your first have to sign in using your credentials at <https://cloud.adminjs.co>

<figure><img src="/files/SF877EahBjfwooEohHVs" alt=""><figcaption><p>Login View</p></figcaption></figure>

You should now see the Cloud Hosting dashboard. Click "Apps" in the sidebar menu to proceed.

<figure><img src="/files/2G8geePCUGdaavSDU6OD" alt=""><figcaption><p>Dashboard</p></figcaption></figure>

If you have any existing applications, they will appear now. If not, click "Create new" to create your first application.

<figure><img src="/files/XUClsknzeBKaOjFayt8S" alt=""><figcaption><p>Applications</p></figcaption></figure>

Fill in the form. Currently, only application's name is required but you may also describe your application if you'd like to.

<figure><img src="/files/z5YW5TeZCkmSA6FA8WCa" alt=""><figcaption><p>Application Creation Form</p></figcaption></figure>

After you press "Save", an application will be created with "Waiting For Approval" status. Once the application is reviewed by an administrator, you can proceed to generate your API key/secret pair.

<figure><img src="/files/0TyrQ0imw9kHbyVUmYiP" alt=""><figcaption><p>Application Details</p></figcaption></figure>

{% hint style="info" %}
All applications come with a URL in "\<app\_name>.adminjs.cloud" format. If you would like to set up a custom domain, contact us.
{% endhint %}


# Generate API Key & Secret

Cloud Hosting service allows you to deploy your applications using a CLI. To use the CLI, you need an API Key & API Secret. To generate these, make sure your application has already been approved by an administrator.

Once your application is approved, you should see an option to generate an API Key & Secret next to your application.

<figure><img src="/files/ttdi3E2txNRFf8UeBcTk" alt=""><figcaption><p>Applications List with context menu</p></figcaption></figure>

Select "Generate API Key" and you should soon see a new screen which allows you to download your API Key/Secret pair in a text file.

<figure><img src="/files/DrFdXLNbciCgImY7FCAb" alt=""><figcaption><p>API Key &#x26; Secret</p></figcaption></figure>

Once you download the file, you can proceed to [deploy your application](broken://pages/4clmK5qFDj4b6lESgWHm) using API Key/Secret pair.

{% hint style="warning" %}
Choosing to "Generate API Key" again invalidates existing API Key/Secret pair.
{% endhint %}

You can view your API Key anytime in your application's details, but if you lose your API Secret you must regenerate both API Key & Secret.

<figure><img src="/files/gOAVF96M1UqgYH8Og3r0" alt=""><figcaption></figcaption></figure>


# Creating and Deploying the application using CLI

AdminJS Cloud Hosting comes with a CLI tool which you can use locally or inside your CI/CD to deploy your application.

## Requierments

Before you will make installation and configuration steps make sure you met all the requirements below.

* Node.js >=18 (we recommend latest LTS version)
* yarn installed globally (`npm install --global yarn`)

## Installation

{% code overflow="wrap" %}

```bash
$ yarn global add @adminjs/cloud-cli
```

{% endcode %}

## Configuration

`@adminjs/cloud-cli` relies on configuration file to be present in your source code. The default file name is `adminjs-cloud.json` but you can provide a custom file path using `--config` option in a CLI command.

### Options

```
include        string[]        A list of files/directories you wish to deploy.
```

### Example

{% code title="adminjs-cloud.json" %}

```json
{
  "include": [
    "public",
    "dist",
    "src",
    ".env",
    "package.json",
    "tsconfig.json",
    "yarn.lock"
  ]
}
```

{% endcode %}

### Starting the application

Currently, AdminJS Cloud Hosting requires `start` script to be present in your `package.json` file. This is the command you use to start your application:

{% code title="package.json" %}

```json
{
  ...,
  "scripts": {
    ...,
    "start": "node app.js"
  }
}
```

{% endcode %}

In the future, we plan to extend application's configuration so that you can provide a custom start command.

## Commands

As of version `1.2.0` the CLI only allows you to create and deploy your application.

To use `@adminjs/cloud-cli` you must first request an application in [Pricing](https://adminjs.co/pricing) page and generate an API Key & API Secret.

### #create

The `create` command allows you to create basic AdminJS application with basic authentication. The CLI generates only code for running it, you have to use commands `yarn && yarn build && yarn start` to check if setup is complete succesfully, if you follow all the steps correctly.

#### Parameters

```
name          string        required        The name of your application
database      string        required        The connection string to database eg. `postgres://adminjs:adminjs@localhost:5432/adminjs`
apiKey        string        required        Your API Key
apiSecret     string        required        Your API Secret
config        string        optional        Path to your configuration file (relative to PWD)
```

#### Usage

```bash
$ adminjs-cloud create --name=<string> --database=<string> --apiKey=<string> --apiSecret=<string>
```

### #deploy

The `deploy` command allows you to deploy your source code. The CLI assumes your code is already built and whatever files you choose to `include` in your configuration file are enough to start your application.

#### Parameters

```
apiKey        string        required        Your API Key
apiSecret     string        required        Your API Secret
config        string        optional        Path to your configuration file (relative to PWD)
```

#### Usage

```bash
$ adminjs-cloud deploy --apiKey=<string> --apiSecret=<string> --config=[string]
```


# Editing environment variables

After your application is approved and prepared by an administrator, you can also edit it's environment variables. To do this, select "Edit Environment Variables" in your application's context menu.

<figure><img src="/files/fo2uAPUoGgl4qWxUc7m1" alt=""><figcaption><p>Application's context menu</p></figcaption></figure>

A new screen will appear where you can add/edit/delete your environment variables. All values are stored as **text**.

<figure><img src="/files/iE0y6ZphifFN48w8przk" alt=""><figcaption><p>Environment Variables</p></figcaption></figure>

After you save your environment variables, your application will be automatically restarted.


# Serverless

AdminJS can be deployed on serverless platforms such as [Vercel](https://vercel.com/) or AWS Lambda.\
However, special attention is needed when it comes to bundling custom components and serving static assets.

This guide will walk you through:

1. **Bundling custom AdminJS components for serverless deployment**
2. **Serving AdminJS static files in a serverless environment**

***

## Bundling AdminJS Components

When deploying AdminJS, any custom React components you add need to be bundled into static files that the client can access.\
AdminJS does not provide an official CLI for this process, but the [`@adminjs/bundler`](https://github.com/SoftwareBrothers/adminjs-bundler) package is recommended.

### Choosing the Right Bundler Version

* If your `tsconfig.json > compilerOptions.module` is **commonjs**, use `@adminjs/bundler@^2.0.0` or before.
* If you are using **ESM** (ECMAScript Modules), use `@adminjs/bundler@^3.0.0` or after.

{% hint style="info" %}
Check your module system before installing the bundler to avoid compatibility issues.
{% endhint %}

#### Example: Using CommonJS (<bundler@2.x>)

**Install**

NPM:

```bash
npm install @adminjs/bundler@^2.0.0
```

Yarn:

```bash
yarn add @adminjs/bundler@^2.0.0
```

**Create your component loader:**

```ts
// src/admin/component/index.ts
import { ComponentLoader } from 'adminjs';

export const componentLoader = new ComponentLoader();
export const components = {
  NotEditableInput: componentLoader.add('NotEditableInput', './NotEditableInput'),
};
```

**Create a bundler entry:**

```ts
// src/bundler.ts
import { bundle } from '@adminjs/bundler';
import { join } from 'path';

void (async () => {
  await bundle({
    customComponentsInitializationFilePath: 'src/admin/component/index.ts',
    destinationDir: 'dist/public',
  });
})();
```

**Add to your build script (e.g. in `package.json`):**

```json
{
  "scripts": {
    "build": "nest build && node dist/bundler.ts"
  }
}
```

Running `yarn build` will output bundled assets in `dist/public`.

***

## Serving Bundled Files in Serverless

### Using Vercel (Recommended Example)

Update your `vercel.json` to serve both your server (API) and static assets:

```json
{
  "version": 2,
  "builds": [
    {
      "src": "dist/main.js",
      "use": "@vercel/node",
      "config": {
        "includeFiles": ["dist/**/*"]
      }
    },
    {
      "src": "dist/public/**/*",
      "use": "@vercel/static",
      "config": {
        "outputDirectory": "dist/public"
      }
    }
  ],
  "routes": [
    { "src": "/public/(.*)", "dest": "/dist/public/$1", "methods": ["GET"] },
    { "src": "/(.*)", "dest": "/dist/main.js" }
  ]
}
```

This configuration ensures:

* Your main server is handled by Vercel’s serverless function.
* Your static assets (custom AdminJS components) are served from `/public/`.

***

## Loading Bundled Assets in AdminJS

In your AdminJS module configuration, make sure to set `assetsCDN` to the correct static URL:

```ts
@Module({
  imports: [
    AdminJsModule.createAdminAsync({
      useFactory: () => ({
        adminJsOptions: {
          rootPath: '/admin',
          assetsCDN: 'https://your-serverless-domain.vercel.app/public/', // Must end with /
        },
      }),
    }),
  ],
})
export class AdminModule implements OnModuleInit {
  async onModuleInit() {
    if (process.env.NODE_ENV === 'development') {
      await adminjs.watch();
    }
  }
}
```


# Legacy documentation

{% hint style="warning" %}
Legacy (outdated) documentation is available here:\
<https://adminjs-docs.web.app>
{% endhint %}


