Documentation

OpenAdminJS developer guide

Layout inspired by framework docs such as Next.js: sticky section nav, searchable sidebar, predictable headings, and copy buttons on every block. The stack pairs a Nest.js API with Next.js apps (admin & web)—start with installation, master defineResource, then ship RBAC-backed CRUD and production deploys.

Quick install
bash
npx openadminjs create my-app

Overview

OpenAdminJS is a resource-first stack for internal tools, CRMs, CMS dashboards and SaaS back offices. You describe your domain with Prisma and resource metadata; the platform wires CRUD, permissions, audit trails and UI chrome around those definitions—similar to how Nest.js modules declare providers and routes once, then compose across the runtime.

Core idea: one defineResource contract powers the generic admin CRUD surface, while the Nest app also exposes auth, dashboards, storefront checkout, BullMQ ops, plugin management and the admin AI service—see Full route map.

When to choose OpenAdminJS

  • You want a resource-driven admin workflow on a Node.js + TypeScript stack.
  • You need RBAC, audit logs and file handling without rebuilding them for every resource.
  • You plan to expose some resources publicly (SEO, marketing pages) from the same models.

Installation

Prerequisites: Node.js 20+, pnpm (recommended) or npm/yarn, and the OpenAdminJS CLI. The flow mirrors a typical Nest.js workspace bootstrap: scaffold, migrate, seed, run. Dependency installation and core env prompts (DB/Redis/JWT) happen automatically during project creation, while admin origin and API port are prefilled with local defaults.

bash
npx openadminjs create my-app
cd my-app
pnpm dev

Default local URLs after pnpm dev:

Adminhttp://localhost:3000
Webhttp://localhost:3001
APIhttp://localhost:4000
OpenAPIhttp://localhost:4000/api/docs

Project structure

Generated monorepos follow a predictable layout so you can jump between API, admin and web the same way you navigate modules in Nest.js.

text
apps/
  api/          Nest.js API — auth, CRUD, files, audit, plugins, AI, jobs, storefront
  admin/        Next.js admin UI generated from resources + ops pages
  web/          Next.js public site, SEO routes, marketing pages

packages/
  core/         defineResource, shared types
  resource/     validation & field helpers
  permissions/  can(), requirePermission()
  auth/         session contracts
  plugin-sdk/   extension surfaces
  storage/      file driver interfaces
  queue/        BullMQ helpers
  notifications/, mail/, ...

prisma/
  schema.prisma
  seed.ts

Configuration

During openadminjs create, the wizard asks for DB credentials and key env values, then writes ready-to-use values into apps/api/.env (including SUPERADMIN_EMAIL / SUPERADMIN_PASSWORD for non-interactive seed). ADMIN_ORIGIN and API_PORT are written with defaults (http://localhost:3000 and 4000) so you only change them when your environment differs. New apps do not get a root .env.example or root .env—treat apps/api/.env as the API env source of truth. Values are read by the Nest API and Next apps through @nestjs/config and NEXT_PUBLIC_* variables—treat secrets like you would in a Nest ConfigModule setup.

.env
DATABASE_URL="postgresql://user:pass@localhost:5432/openadminjs"
JWT_SECRET="change-me-use-long-random-string"
NEXT_PUBLIC_API_URL="http://localhost:4000"
NEXT_PUBLIC_SITE_URL="http://localhost:3001"
  • JWT_SECRET must be unique per environment; rotate if leaked.
  • NEXT_PUBLIC_* is embedded in browser bundles—never place private keys there.

Workspace scripts

Generated apps use pnpm filters so you run database work against the API package and dev servers in parallel—same idea as nx run-many or Nest workspaces.

bash
pnpm dev                 # API + admin + web (see root package.json)
pnpm db:migrate          # run Prisma migrations manually when schema changes
pnpm db:seed             # run seed manually (reseed / reset flows)
pnpm test                # all workspace tests
pnpm --filter @openadminjs/api build

OpenAdminJS commands are run from your project workspace (for example via pnpm exec openadminjs ...). Database shortcuts still map to the same underlying pnpm and Prisma commands.

CLI reference

Use the packaged CLI from your project workspace via pnpm exec openadminjs ... (or through npm scripts).

Project creation

Scaffolds a monorepo with apps/api, apps/admin, apps/web, shared packages, Prisma schema, and automatically installs dependencies with the selected package manager.

bash
npx openadminjs create my-app
cd my-app

generate resource / make resource

Emits a starter *.resource.ts next to your other resources. Model name is PascalCase (Prisma model); the file uses defineResource with kebab-case name and permission keys.

bash
pnpm exec openadminjs generate resource Invoice --force
pnpm exec openadminjs make resource Invoice   # alias

Always register the new file in registry.ts, extend Prisma if needed, then migrate.

generate plugin / make plugin

Scaffolds a starter Plugin SDK module under apps/api/src/plugins/custom/. Wire the plugin into apps/api/plugins.manifest.json and grant the capability flags your code needs ( resource.hooks, api.routes, seo.extend, …).

bash
pnpm exec openadminjs generate plugin com.example.my-extension --force
pnpm exec openadminjs make plugin com.example.my-extension

dev, build, start

Convenience wrappers around pnpm that run API + admin (not web, unless your root scripts differ). Matches the binaries shipped alongside openadminjs create.

bash
pnpm exec openadminjs dev       # parallel dev for api + admin
pnpm exec openadminjs build    # recursive build
pnpm exec openadminjs start    # production-style start script

security check

Static checklist over env snippets and minimal admin-route presence (apps/admin/app/login/page.tsx).

bash
pnpm exec openadminjs security
pnpm exec openadminjs security check

doctor

Runs quick health checks for a generated workspace: package.json, pnpm-workspace.yaml, prisma/schema.prisma, and either apps/api/.env or a root .env.example.

bash
pnpm exec openadminjs doctor

db migrate | seed | studio

These commands print the exact pnpm / Prisma invocations to run from your project root so you do not guess package names.

bash
pnpm exec openadminjs db migrate
pnpm exec openadminjs db seed
pnpm exec openadminjs db studio

Prisma & migrations

Edit prisma/schema.prisma (or fragment files merged into it), then apply migrations from the workspace root. Regenerate the client after every schema pull or merge.

bash
# after editing models
pnpm db:migrate
pnpm prisma generate

# optional: open GUI
pnpm --filter @openadminjs/api exec prisma studio
  • Keep Prisma model names aligned with model on each resource (model: "invoice" → Prisma model Invoice maps to delegate invoice).
  • Example repos under examples/ show models.fragment.prisma merged into a full schema—use the same pattern for modular schemas.

Creating resources (end-to-end)

A resource connects one Prisma model to generic list/create/edit/delete routes, admin UI tables and forms, validation, and RBAC. Field keys in fields must match your Prisma column names (for example categoryId for a foreign key).

  1. Model the table in Prisma — add or extend a model in prisma/schema.prisma, then run pnpm db:migrate and pnpm prisma generate.
  2. Scaffold a resource module (optional) — generates apps/api/src/resources/<name>.resource.ts with sensible defaults:
    bash
    pnpm exec openadminjs generate resource Invoice
    pnpm exec openadminjs make resource Invoice   # alias
  3. Implement defineResource — set name (URL segment, kebab-case), model (Prisma delegate, camelCase), permissions (at minimum read), and a fields map. See the full reference in Resource metadata and Field properties.
  4. Register the module — import the default export in apps/api/src/resources/registry.ts and add it to the resources array (keep imports sorted).
  5. Grant permissions — seed roles so users receive the matching strings (invoices.read, …). Until <name>.read is on the JWT, the resource is hidden from the catalog.
  6. Verify — restart pnpm dev, open the admin, and confirm list + forms. Inspect GET /admin/resources with a token to see merged metadata (locale, stripped sensitive fields).
Canonical example: follow apps/api/src/resources/posts.resource.ts for localized labels ({ en, ru }), relations (resource + displayField), media pointing at resource: "files", seo, and a working actions.publish handler.

Resources

A resource maps a Prisma model to admin/API behavior: labels, fields, permissions, optional actions and visibility flags. Resource names use kebab-case (order-items); Prisma model names stay camelCase in the model field (orderItem). The Nest side resolves (prisma as any)[resource.model], so model must match the generated client delegate name exactly.

TypeScript
import { defineResource } from "@openadminjs/core";

export default defineResource({
  name: "posts",
  label: "Posts",
  model: "post",
  titleField: "title",
  icon: "FileText",
  permissions: {
    read: "posts.read",
    create: "posts.create",
    update: "posts.update",
    delete: "posts.delete"
  },
  fields: {
    id: { type: "id", label: "ID", create: false, edit: false, list: false },
    title: { type: "text", label: "Title", required: true, searchable: true, sortable: true },
    slug: { type: "slug", label: "Slug", from: "title", searchable: true },
    status: {
      type: "select",
      label: "Status",
      options: ["draft", "published", "archived"],
      filterable: true,
      sortable: true
    },
    content: { type: "richtext", label: "Body", list: false },
    createdAt: { type: "datetime", label: "Created", create: false, edit: false, sortable: true }
  }
});

Registry & Prisma

Register every resource in apps/api/src/resources/registry.ts (the same pattern as collecting Nest providers in a module). After changing resources or Prisma models, run migrations so delegates stay in sync.

TypeScript
import type { ResourceConfig } from "@openadminjs/core";
import posts from "./posts.resource";

export const resources = [posts] satisfies ResourceConfig[];

export function getResource(name: string): ResourceConfig | undefined {
  return resources.find((r) => r.name === name);
}

defineResource metadata reference

The object passed to defineResource implements ResourceConfig (from @openadminjs/core). Invalid name / model values throw at module load so misconfigurations fail fast.

PropertyRequiredPurpose
nameYesURL segment and API key — kebab-case, ^[a-z][a-z0-9-]*$.
labelYesHuman title; string or map of BCP-47 locale → string (see Internationalization).
modelYesPrisma delegate name (post, orderItem) — must exist on PrismaClient.
fieldsYesMap of Prisma column name → ResourceField (at least one column).
permissionsYesString map; must include read. Other keys (create, update, delete, custom verbs) gate HTTP verbs and actions.
titleFieldNoWhich column labels rows in lists and relation pickers when no display value is passed.
iconNoName of a lucide-react icon for the admin chrome (e.g. FileText).
actionsNoNamed row operations with server handlers (publish, revoke, …).
i18nNodefaultLocale + supported locales[]. Labels are localized via label: { en, … } on the resource and each field.
seoNoPublic-site metadata helpers: paths, slug/title/description fields for shared models.
listScopeNoRestrict list queries for some callers — see List scope.

Resource actions

Beyond CRUD, attach named actions with labels, optional confirmation, variant styling, and a permission string. Handlers receive Prisma, the current user, and optional input—use them for transitions that should not be a raw PATCH (publish, archive, refund, rotate key).

TypeScript
export default defineResource({
  name: "posts",
  label: "Posts",
  model: "post",
  titleField: "title",
  permissions: {
    read: "posts.read",
    create: "posts.create",
    update: "posts.update",
    delete: "posts.delete",
    publish: "posts.publish"
  },
  fields: { /* ... */ },
  actions: {
    publish: {
      label: "Publish",
      variant: "primary",
      confirm: true,
      permission: "posts.publish",
      async handler({ id, prisma, user }) {
        // await prisma.post.update({ where: { id }, data: { status: "published" } });
        return { ok: true, by: user.email };
      }
    }
  }
});

Expose actions through the admin API (see Custom actions) and render them in the admin UI when the user has the required permission.

Fields

Each entry in fields uses the Prisma column name as the object key (authorId, coverImageId). Every field declares a type (determines widgets and API validation rules) plus optional UX and query flags described below.

Visibility in admin & API

Boolean flags drive where a column appears. If you omit a flag, OpenAdminJS treats it as “allowed” — except where types such as id, computed, or password receive special handling (see sensitive fields).

FlagControls
listList/table columns. Set list: false for long text or JSON.
detailRead-only detail view. Omit = shown; use false to hide.
createPOST body acceptance + create form controls. Server skips fields with create: false.
editPATCH acceptance + edit form controls.
searchableIncluded in OR text search for list endpoints (use sparingly on large columns).
sortableAllows sort=field:asc|desc for that column.
filterableAllows structured filter[field][op]= query params on list endpoints.
Sensitive & internal values: defineResource runs withSafeFields — column names matching password/token patterns, fields marked sensitive: true, and types password / hidden are removed from list/detail exposure. The API also strips hidden and sensitive values from JSON responses. id is never writable; computed is read-only in forms.

Every ResourceField property

PropertyTypical use
typeRequired — one of the field types (below).
labelString or locale map ({ en, ru }) for captions in admin + API meta.
requiredMarks create-time requirement (validated server-side).
list, detail, create, editVisibility toggles (table above).
searchable, sortable, filterableList/query capabilities.
optionsAllowed discrete values for select, multiselect, badge.
resourceFor relation, image, file: target resource name ("categories", "files").
displayFieldFor relation: column on the related row to show in pickers (default name).
fromFor slug: column to auto-fill from when the form creates a slug.
permissionsPer-field RBAC overrides (read, create, update permission strings).
sensitiveForces sanitization/redaction pipelines.
defaultValueSeeds UI defaults on create flows.
min, maxNumeric (number, money) validation.
minLength, maxLengthString field validation (text, email, slug, …).

Field type → behavior

typeStored asAdmin / API notes
idPrimary keyNever writable; omit from writes.
text, textareastringValidated length via minLength/maxLength.
email, urlstringFormat validation on create/edit.
slugstringLowercase hyphenated pattern; optional from source field.
passwordstring (hashed server-side)Write-on-create/edit only; never exposed in list/detail.
number, moneynumberSupports min/max.
booleanbooleanRequired booleans must be sent explicitly on create.
select, badgestringValues constrained to options when provided.
multiselectstring[]Accepts JSON array or comma-separated string in forms.
date, datetimeDate / ISO stringCoerced by server validation.
relationscalar FKPickers load GET /admin/resources/<resource>; value is the related row id.
image, fileFK or URL (project convention)Use resource: "files" when storing uploaded asset ids (see posts.resource.ts).
richtext, markdownstringHeavy content — usually list: false.
jsonJSONArbitrary structure; avoid searchable.
colorstringColor picker widget.
codestringMonospace editor for snippets.
hiddenanyServer-managed; omitted from API responses.
computedderivedNot part of create/edit payloads; display-only when populated.

id, text, textarea, number, boolean

TypeScript
id: { type: "id", label: "ID", create: false, edit: false, list: true },
title: { type: "text", label: "Title", required: true, searchable: true, sortable: true },
notes: { type: "textarea", label: "Notes", list: false },
priceCents: { type: "number", label: "Price (¢)", sortable: true },
featured: { type: "boolean", label: "Featured", filterable: true }

email, url, slug

slug can derive from another field via from for auto-slugging in forms.

TypeScript
contactEmail: { type: "email", label: "Contact", searchable: true },
website: { type: "url", label: "Website" },
slug: { type: "slug", label: "Slug", from: "title", searchable: true }

password, money, color, badge

TypeScript
password: { type: "password", label: "Password", create: true, edit: true, list: false },
amount: { type: "money", label: "Amount", sortable: true },
theme: { type: "color", label: "Accent" },
status: { type: "badge", label: "Status", options: ["open", "closed"] }

select & multiselect

TypeScript
status: {
  type: "select",
  label: "Status",
  options: ["draft", "published", "archived"],
  filterable: true,
  sortable: true
},
tags: {
  type: "multiselect",
  label: "Tags",
  options: ["news", "product", "changelog"],
  filterable: true
}

date & datetime

TypeScript
publishDay: { type: "date", label: "Publish date", sortable: true },
createdAt: { type: "datetime", label: "Created", create: false, edit: false, sortable: true }

relation, image, file

TypeScript
categoryId: {
  type: "relation",
  label: "Category",
  resource: "categories",
  displayField: "name",
  filterable: true
},
coverImageId: {
  type: "image",
  label: "Cover",
  resource: "files",
  list: false
},
manualPdfId: {
  type: "file",
  label: "PDF",
  resource: "files",
  list: false
}

richtext & json

TypeScript
body: { type: "richtext", label: "Body", list: false },
metadata: { type: "json", label: "Metadata", list: false }

markdown & code

TypeScript
readme: { type: "markdown", label: "README", list: false },
snippet: { type: "code", label: "Snippet", list: false }

hidden & computed

Use hidden for internal keys; computed for derived display columns that may be filled server-side.

TypeScript
legacyId: { type: "hidden", label: "Legacy", list: false, create: false },
fullName: { type: "computed", label: "Full name", list: true, create: false, edit: false }

Field-level permissions & defaults

Optional permissions.read | create | update on a field gate that column independently of the resource-level permission map. defaultValue seeds create forms.

TypeScript
internalScore: {
  type: "number",
  label: "Score",
  permissions: { read: "posts.score.read", update: "posts.score.write" }
},
locale: { type: "select", label: "Locale", options: ["en", "de"], defaultValue: "en" }

Permissions

Permissions are plain strings, conventionally resource.action for CRUD (posts.read) plus any custom verbs you add on actions (posts.publish). Store them in your database, attach to roles, and issue JWTs (or session payloads) that include the flattened list of strings for the signed-in user.

Resource permission map

Every resource must define at least read; create, update, and delete gate the matching HTTP verbs. Extra keys align with actions entries.

Runtime helpers

TypeScript
import { can, requirePermission } from "@openadminjs/permissions";

const userPerms = ["posts.read", "posts.update"];

can(userPerms, "posts.read"); // true
requirePermission(userPerms, "posts.delete"); // throws ForbiddenError

visibleFields()

Use visibleFields(resource, mode, userPerms) from @openadminjs/core (re-exported from @openadminjs/resource) when shaping payloads: it removes sensitive columns and honors per-field permissions for list/create/edit/detail modes.

TypeScript
import { visibleFields } from "@openadminjs/core";

const fields = visibleFields(postsResource, "list", user.permissions);

Seeding example

TypeScript
// prisma/seed.ts (illustrative)
await prisma.role.create({
  data: {
    name: "Editor",
    permissions: {
      connect: [
        { key: "posts.read" },
        { key: "posts.update" },
        { key: "posts.publish" }
      ]
    }
  }
});

List scope (per-user row filtering)

Some resources should only list rows “owned” by the current user until an operator has a broader permission. Set listScope with type: "userOwns" and the foreign-key column that references the user (userId, ownerId, …). The API merges { [field]: signedInUserId } into Prisma where when the scope applies.

  • Bypass — users with "*" on the JWT skip list scoping.
  • bypassPermissions — optional extra permission strings (for example notifications.read.all) that disable the scope for specific roles.
  • Detail / write — list scope filters collection queries; combine with normal RBAC checks on single-record routes.
TypeScript
export default defineResource({
  name: "notifications",
  label: "Notifications",
  model: "notification",
  titleField: "title",
  permissions: { read: "notifications.read", update: "notifications.update" },
  listScope: {
    type: "userOwns",
    field: "userId",
    bypassPermissions: ["notifications.read.all", "*"]
  },
  fields: {
    // ...
  }
});

Live pattern: see apps/api/src/resources/operations.resource.ts (notificationsResource).

HTTP API — full route map

Base URL is commonly http://localhost:4000. OpenAPI is served at /api/docs. Most admin routes require Authorization: Bearer <access> and the admin JWT realm (@RequireAuthRealm("admin")), unless noted. A global throttle applies baseline rate limits (defaults: 100 requests / 60s per key—see ThrottlerModule in app.module.ts).

OpenAPI

HTTP
GET /api/docs               # Swagger UI
GET /health                  # Nest health ping
Prefix / areaEndpoints (summary)Auth & notes
/auth POST login (throttled), POST refresh, POST logout, GET me, GET me/realms (admin realm only), POST password/forgot, POST password/reset, POST email/verify Mixed bearer + anonymous; realm from body defaults via DEFAULT_AUTH_REALM.
/admin/resources GET /overview (dashboard aggregates), GET / resource catalog (+ optional ?locale=), CRUD under /:resource, POST …/orders/:id/refund (payment bridge), POST …/:resource/:id/actions/:action Admin realm JWT; refunds require working PaymentModule.
/admin/plugins GET /, POST /, PATCH /:id, DELETE /:id Admin realm; manifests drive runtime loader.
/admin/ai GET|PUT …/config, POST …/chat, POST …/chat/stream (SSE), conversations CRUD, GET …/artifacts/:id/download Admin realm; chat paths require permission ai.chat in service guards.
/jobs GET /stats (jobs.read), POST /dispatch (jobs.dispatch) Admin realm + explicit permission decorators.
/store GET /products, POST /checkout, GET /orders/:id/status, POST /webhook/:provider, plus GET /session & GET /orders (public realm JWT) Hybrid: public storefront + Stripe-style webhooks via signature headers.

Plugins with api.routes or interceptors integrate through bootstrap ( see PluginApiInterceptor registered in app.module.ts). Outbound mail is internal (MailModule / MailService)—there is no public mail REST surface.

Authentication

The API uses JWT access + refresh tokens. Login and refresh accept a realm string (defaults align with DEFAULT_AUTH_REALM); back-office users use "admin" while storefront sessions use "public" for routes like GET /store/session.

HTTP
POST /auth/login
Content-Type: application/json

{ "email": "<superadmin-email>", "password": "<superadmin-password>", "realm": "admin" }
HTTP
POST /auth/refresh
Content-Type: application/json

{ "refreshToken": "<jwt>", "realm": "admin" }
HTTP
GET /admin/resources/posts
Authorization: Bearer <access_token>
Accept: application/json

What ships on /auth

  • POST /auth/login — throttle-limited credential exchange (realm aware).
  • POST /auth/refresh — rotate access tokens.
  • POST /auth/logout — revoke session server-side metadata (Bearer required).
  • GET /auth/me — hydrate profile + permissions snapshot.
  • GET /auth/me/realms — admin-only rollup of memberships across realms.
  • POST /auth/password/forgot & /auth/password/reset — token-based reset funnel (email delivery via MailService).
  • POST /auth/email/verify — verify email confirmation tokens.
  • Rotate JWT_SECRET / JWT_REFRESH_SECRET with a maintenance window; invalidate stale refresh tokens.
  • Never log access tokens; scrub them from client error reporters.

Admin REST surface

Authenticated admin routes follow a predictable resource pattern. Collection routes return paginated rows plus column metadata derived from defineResource; singleton routes hydrate a single Prisma record through the same permission gates. The catalog endpoint (GET /admin/resources) accepts an optional ?locale= parameter so labels resolve through resolveResourceForLocale before the Next admin hydrates tables.

HTTP
GET    /admin/resources              # catalog (+ ?locale=)

Dashboard overview

Metrics for the default admin dashboard (totals, mocked marketing funnels, audit trail preview, job health) are aggregated in AdminAnalyticsService.

HTTP
GET /admin/resources/overview?period=30d
Authorization: Bearer <token>

# period accepts 7d | 30d | 90d
HTTP
GET    /admin/resources/:resource    # list rows
GET    /admin/resources/:resource/:id
POST   /admin/resources/:resource
PATCH  /admin/resources/:resource/:id
DELETE /admin/resources/:resource/:id

Replace :resource with the kebab-case resource name (for example order-items). IDs use your Prisma primary key format (commonly cuid/uuid).

Order refunds: POST /admin/resources/orders/:id/refund proxies to PaymentService when the commerce module loads. It is intentionally separate from defineResource actions—treat it as ops glue for payment providers you enable in your deployment.

CRUD & request bodies

Create (POST)

Send JSON keys matching writable fields from the resource definition. Server validation rejects unknown keys and required field violations before Prisma create.

HTTP
POST /admin/resources/posts
Authorization: Bearer <token>
Content-Type: application/json

{
  "title": "Launch checklist",
  "status": "draft",
  "slug": "launch-checklist"
}

Update (PATCH)

Partial updates merge into the existing row; fields with edit: false are stripped even if a client sends them.

HTTP
PATCH /admin/resources/posts/clxyz123
Authorization: Bearer <token>
Content-Type: application/json

{ "status": "published" }

Delete

HTTP
DELETE /admin/resources/posts/clxyz123
Authorization: Bearer <token>

Errors

Expect 401 for missing/invalid auth, 403 when permissions fail, 404 when the resource name or row id is unknown, and 422 for validation errors with a structured body your forms can map to fields.

Queries & filters

List endpoints accept pagination, free-text search (against fields marked searchable: true), sorting for sortable columns, and equality filters for filterable fields.

HTTP
GET /admin/resources/posts?page=1&limit=20&sort=createdAt:desc&search=launch&filter[status]=published
  • page / limit — cursor-friendly pagination; keep limits ≤ 100 in production unless your gateway allows higher.
  • sort — format field:asc|desc; reject sorts on non-sortable fields.
  • search — OR-across searchable string fields at the Prisma delegate level.
  • filter[field] — legacy equality shorthand; prefer filter[field][eq]=, [gte], [in] (comma-separated) for operators enforced in AdminResourceService.buildWhere.

Custom actions

POST to /admin/resources/:resource/:id/actions/:actionName where actionName matches a key under resource.actions. The handler runs inside the same Nest transaction boundaries as CRUD so audit hooks stay consistent.

HTTP
POST /admin/resources/posts/clxyz123/actions/publish
Authorization: Bearer <token>
Content-Type: application/json

{ "note": "Approved by editor" }

Optional JSON bodies surface as input on ResourceActionContext—validate them with Zod/class-validator before touching Prisma.

Plugins, background jobs & AI endpoints

Plugins (/admin/plugins)

Runtime plugin metadata is manipulated through REST while the authoritative manifest remains apps/api/plugins.manifest.json. After changing entries you may need to restart the API depending on loader behavior (AdminPluginsService).

HTTP
GET    /admin/plugins
POST   /admin/plugins
PATCH  /admin/plugins/:id
DELETE /admin/plugins/:id

Queues (/jobs)

Operational hooks for observing BullMQ / worker health (@openadminjs/queue integration). Requires explicit permission grants on the JWT.

HTTP
GET  /jobs/stats      # Permission: jobs.read
POST /jobs/dispatch   # Permission: jobs.dispatch

Assistant (/admin/ai)

Conversation persistence, streamed completions, downloadable artifacts. Calls into chat/stream require the ai.chat permission on the authenticated admin user (AdminAiService.ensureChatPermission). Configuration is persisted via mutable settings payloads—tokens should never be echoed to browser logs.

HTTP
GET    /admin/ai/config
PUT    /admin/ai/config
POST   /admin/ai/chat
POST   /admin/ai/chat/stream        # SSE: text/event-stream
GET    /admin/ai/conversations
GET    /admin/ai/conversations/:id
PATCH  /admin/ai/conversations/:id
DELETE /admin/ai/conversations/:id
GET    /admin/ai/artifacts/:id/download

Streaming payloads mirror the envelopes described under AI streaming events.

Storefront HTTP API (/store)

Example commerce routes coexist with generic admin resources. Checkout can run anonymously (guest checkout) while authenticated shoppers send the same Bearer token minted from the public realm (@RequireAuthRealm("public")). Webhooks authenticate via provider signatures forwarded as stripe-signature or x-webhook-signature; raw bodies must remain unparsed upstream for Stripe compatibility.

HTTP
# Public storefront
GET  /store/products
POST /store/checkout
GET  /store/orders/:id/status
POST /store/webhook/:provider

# Authenticated storefront (JWT, public realm)
GET /store/session
GET /store/orders?page=1&limit=20
  • Prefer idempotent webhook handlers keyed by provider event ids.
  • Keep payment secrets strictly server-side (.env on the Nest process).

Admin UI

The Next.js admin app consumes the same resource metadata as the API: table columns mirror list: true fields, filters bind to filterable flags, and create/edit dialogs call the same REST routes your integration tests hit. Customize branding under apps/admin (layouts, design tokens, logo) without forking the resource contract.

  • Dashboards aggregate metrics per resource; add cards by extending the admin shell layout.
  • Forms respect create/edit field flags, required, defaultValue, and sensitive field stripping from withSafeFields.
  • Relations render searchable pickers using resource + displayField metadata.
  • Actions render as primary/destructive buttons when actions are defined and the JWT includes the action permission.

Point the admin at your API with NEXT_PUBLIC_API_URL; session cookies or bearer tokens are injected by the generated auth provider.

Shipped routes in apps/admin/app

Besides dynamic CRUD screens, these App Router segments ship in the scaffold:

text
/                 — marketing / redirect landing
/dashboard        — KPIs wired to GET /admin/resources/overview
/login            — session acquisition
/profile          — user profile shell
/settings         — editable platform settings surfaced through resources
/files            — file asset explorer (requires files.* permissions)
/audit-logs       — immutable audit feed
/notifications    — inbox pattern
/api-tokens       — issued token management + revoke actions
/plugins          — manifest editor + sandbox controls
/jobs             — queue stats / dispatch tooling
/resources/*      — generated list/detail/create/edit driven by metadata

Admin navigation model

The shell layout is the operational backbone of the admin app. Keep global concerns (theme switcher, locale switcher, profile, AI widget, quick actions) in apps/admin/components/app-shell.tsx, and keep page-level logic in route files. This separation avoids coupling resource pages to global UI concerns.

  • Sidebar entries should mirror real resource groups (content, users, billing, settings) instead of raw table names.
  • Top-level actions should be idempotent where possible (refresh, sync, regenerate cache).
  • Route params should stay resource-centric (/resources/:resource/:id) so links can be generated by metadata.

Admin forms & field rendering

Form rendering is metadata-driven. Prefer enriching resource field metadata over custom per-page form logic: this keeps create/edit/detail screens consistent across resources.

TypeScript
fields: {
  title: { type: "text", required: true, list: true, create: true, edit: true },
  status: { type: "select", options: ["draft", "published"], list: true, filterable: true, sortable: true },
  authorId: { type: "relation", resource: "users", displayField: "email", create: true, edit: true }
}
  • Validation source of truth stays server-side (Prisma + API validation); UI should map returned field errors to inputs.
  • Sensitive fields (sensitive: true) must never be shown in list/detail responses.
  • Relation fields should provide searchable pickers with predictable labels and fallback values.

Admin operations & diagnostics

For operational reliability, every mutation path should surface explicit feedback in UI: success toast, actionable error, and structured diagnostics for failed workflows.

  • Delete flows should use confirmation modals or guarded chat confirmations for destructive actions.
  • Long-running operations should stream step status (queued, running, completed, failed).
  • Workflow failures should display failedStep, partialResults, rolledBack, and rollbackFailures.
  • Retry UX should provide one-click rerun for last failed workflow payload where safe.

AI assistant

OpenAdminJS includes a built-in admin AI assistant with provider configuration (GET/PUT /admin/ai/config), streaming replies, action execution, conversation storage, and downloadable artifacts. Chat entry points require permission ai.chat (enforced in AdminAiService).

Provider configuration

Providers are edited through the REST config endpoint and mirrored in the Next admin settings UI when present. Secrets are stored via the API and must never be echoed into client bundles or static exports.

  • Built-in provider patterns: OpenAI/compatible, Anthropic, Gemini.
  • Active provider switch allows runtime fallback without code changes.
  • System prompt should stay in English and enforce JSON action schema output.

AI streaming events

Chat streaming uses SSE with normalized event envelopes shared by API and admin app. Each stream emits token deltas and a final completion payload with optional action metadata.

JSON
{ "type": "delta", "delta": "Partial token..." }
{ "type": "done", "reply": "Final text", "actionResult": { "ok": true } }
{ "type": "error", "message": "Provider timeout", "code": "AI_STREAM_ERROR" }
  • delta appends text progressively for typing effect.
  • done finalizes message and can include pending/confirmed action payloads.
  • error must surface visibly in UI and stop loading state immediately.

AI workflows & rollback

Workflow actions chain multiple steps (for example: generate image → create artifact → update resource). Use transactionMode to control failure behavior:

  • best_effort — keep completed steps even if a later step fails.
  • rollback_on_error — attempt compensation for completed reversible steps.

On failure, return rich diagnostics so operators can understand exactly what happened and safely retry: failedStep, completedSteps, partialResults, rolledBack, rollbackFailures.

Guardrails: Require explicit user confirmation before destructive operations (delete, revoke, irreversible side effects), and always enforce server-side permission checks even if the assistant proposes the action.

Web layer (apps/web)

The secondary Next.js app hosts marketing and content routes: landing pages, resource-driven listings and detail pages backed by read-only fetches or ISR. It is separate from the Nest storefront API under /store (checkout, webhooks, session) documented in Storefront HTTP API—compose them when you need marketing pages plus headless commerce callbacks.

text
apps/web/app/
  page.tsx
  posts/page.tsx
  posts/[slug]/page.tsx
  sitemap.xml/route.ts
  robots.txt/route.ts

Use the generated openadmin-client helpers to fetch published rows by slug while keeping payloads shaped by the same field metadata used in admin.

SEO helpers

Use helper utilities to keep canonical URLs, OpenGraph tags and sitemap entries aligned with resource definitions so marketing pages inherit titles and descriptions from your CMS models.

TypeScript
import { metadataForRecord, sitemapXml } from "../lib/seo";

export function generateMetadata({ params }) {
  const record = await loadPost(params.slug);
  return metadataForRecord(postResource, record);
}

Emit sitemap.xml by iterating public resources and mapping slug or path fields; block indexing on staging via robots.txt using NEXT_PUBLIC_SITE_URL.

Internationalization (i18n)

Labels on the resource and on each field use the shared LocalizedLabel type: either a plain string or a map of BCP-47 locale codes → string ({ en: "Title", ru: "Заголовок" }). The Nest API merges the requested locale via resolveResourceForLocale() (@openadminjs/core) before returning catalog metadata used by Next.js apps.

There is no i18n.translations blob. Put each locale's wording directly on label (resource, fields, action labels). The optional i18n block only pins defaultLocale and declares the allowed locales[] tuple used when resolving fallback order.

Localized labels in defineResource

Copy the production pattern from apps/api/src/resources/posts.resource.ts:

TypeScript
export default defineResource({
  name: "posts",
  label: { en: "Posts", ru: "Записи" },
  model: "post",
  titleField: "title",
  i18n: {
    defaultLocale: "en",
    locales: ["en", "ru"]
  },
  permissions: {
    read: "posts.read",
    create: "posts.create",
    update: "posts.update",
    delete: "posts.delete"
  },
  fields: {
    title: {
      type: "text",
      label: { en: "Title", ru: "Заголовок" },
      required: true
    },
    status: {
      type: "select",
      label: { en: "Status", ru: "Статус" },
      options: ["draft", "published"]
    }
  },
  actions: {
    publish: {
      label: { en: "Publish", ru: "Опубликовать" },
      permission: "posts.publish",
      async handler(ctx) {
        /* ... */
      }
    }
  }
});

The Next.js admin app sends the viewer's locale with each resource fetch (typically via a ?locale=ru query param persisted from lib/locale.ts), so the API can return resolved strings instead of forcing the UI to duplicate translation tables.

Adding a new locale end-to-end

  1. Expand apps/admin/lib/locale.ts — add the locale to SUPPORTED_UI_LOCALES and LOCALE_LABELS so the shell knows how to persist and display it.
  2. Extend each resource's locale maps — bump i18n.locales plus every relevant label: { en, ru, … } payload (resource root, fields, actions).
  3. Prefer one reload after switching — call setUiLocale("de") from a Settings control, reload, and rely on downstream resource requests carrying the chosen locale automatically.

Plugin system

OpenAdminJS Plugin Platform is a broad extension system (WordPress-style) with explicit capability gating. Plugins can register resource hooks, API hooks/routes, media pipeline transforms, SEO contributors, job handlers, and admin UI extensions. Plugins are loaded from plugins.manifest.json and enforced by trust mode + capabilities.

TypeScript
import type { OpenAdminPlugin } from "@openadminjs/plugin-sdk";

export const seoAndMediaPlugin: OpenAdminPlugin = {
  id: "com.example.seo-media",
  version: "1.0.0",
  async register({ registerSurface }) {
    registerSurface({
      seo: {
        metadata({ resourceName, record }) {
          if (resourceName !== "posts") return {};
          return { title: record.title ?? "Untitled" };
        }
      },
      media: {
        transform({ filename, mimeType, contentBase64 }) {
          if (!mimeType.startsWith("image/")) return { filename, mimeType, contentBase64 };
          return { filename: filename.replace(/\\.(png|jpe?g)$/i, ".webp"), mimeType: "image/webp", contentBase64 };
        }
      }
    });
  }
};

Register plugin entries in apps/api/plugins.manifest.json with explicit capabilities:

TypeScript
{
  "version": 1,
  "plugins": [
    {
      "id": "com.example.seo-media",
      "enabled": true,
      "package": "@example/openadmin-seo-media",
      "trustMode": "sandboxed",
      "capabilities": ["seo.extend", "media.pipeline"]
    }
  ]
}

Capability matrix: resource.hooks, api.hooks, api.routes, media.pipeline, seo.extend, jobs.run, admin.ui.extend. Use sandboxed trust mode and request only minimal capabilities by default.

Resource hooks

Resource lifecycle hooks are now one surface within Plugin Platform. Use capability resource.hooks and register through plugin SDK context.

TypeScript
registerResourceHooks("users", {
  async beforeCreate({ data }) {
    if (typeof data.email === "string") data.email = data.email.trim().toLowerCase();
  }
});

Available lifecycle events: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete.

Testing guide

Every workspace package ships Vitest as the test runner. Run all suites from the root with pnpm test; run a single package with pnpm --filter @openadminjs/api test. The E2E smoke suite runs separately with pnpm e2e.

API unit tests

Mock Prisma and JWT services at the spec level — no database required for unit tests.

TypeScript
// apps/api/src/auth/auth.service.test.ts
import { describe, it, expect, vi } from "vitest";
import { AuthService } from "./auth.service";

const mockPrisma = { user: { findUnique: vi.fn() } };
const mockJwt = { signAsync: vi.fn().mockResolvedValue("tok") };

const svc = new AuthService(mockPrisma as any, mockJwt as any);

it("rejects unknown email", async () => {
  mockPrisma.user.findUnique.mockResolvedValue(null);
  await expect(svc.login({ email: "x@x.com", password: "p", realm: "admin" }))
    .rejects.toMatchObject({ status: 401 });
});

Admin component tests

Use @testing-library/react inside a @vitest-environment jsdom block for Next.js client components.

TypeScript
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { AuthGuard } from "../components/auth-guard";
import { vi } from "vitest";

vi.mock("../lib/api", () => ({ hasUsableAccessToken: () => false }));
vi.mock("next/navigation", () => ({ usePathname: () => "/dashboard", useRouter: () => ({ replace: vi.fn() }) }));

it("redirects to login when no token", () => {
  render(<AuthGuard><div>protected</div></AuthGuard>);
  expect(screen.queryByText("protected")).toBeNull();
});

E2E smoke tests

Run pnpm e2e to execute the auth-flow E2E tests (Vitest + jsdom mock of fetch + localStorage) followed by the HTTP smoke script. The smoke script hits /health and /auth/login on the running API and reports pass/fail.

bash
pnpm dev &            # start API + admin
pnpm e2e              # auth flow tests + HTTP smoke checks

Theming & dark mode

The admin UI uses Tailwind CSS with darkMode: ["class"] and next-themes for SSR-safe theme persistence. A sun/moon toggle in the header switches between light, dark, and system modes; the chosen value is stored in localStorage under theme.

Design tokens

Brand colors and surface values are defined as CSS custom properties in apps/admin/app/globals.css and overridden inside the .dark selector. Extend them to align with your product brand without touching Tailwind config.

CSS
:root {
  --brand-blue:      #2454ff;
  --brand-teal:      #0ea5a4;
  --admin-surface:   #ffffff;
  --admin-bg:        #f8fafc;
}

.dark {
  --brand-blue-soft: #1e2a4a;
  --admin-surface:   #0f172a;
  --admin-bg:        #0a0f1e;
}

Logo in dark mode

The <Logo> component reads resolvedTheme from next-themes and switches between the color master logo and the monochrome (white) variant automatically — no extra CSS needed.

TypeScript
// apps/admin/components/logo.tsx
const isDark = resolvedTheme === "dark";
return <img src={isDark ? BRAND.logoMonochromePng : BRAND.logoNew} alt="OpenAdminJS" />;

Extending the theme

  1. Change --brand-blue and --brand-teal in globals.css to match your product's primary colors.
  2. Update tailwind.config.ts colors.brand to match the new values so Tailwind utility classes stay in sync.
  3. Replace logo files in apps/admin/public/brand/; keep both a color master and a monochrome/dark variant.
  4. Adjust gradient stops in app-shell.tsx (promo banner, avatar gradient) from from-[#2454ff] to-[#0ea5a4] to your brand pair.

Security

  • Enforce permission checks on every mutating route (never trust the UI alone).
  • Keep JWT secrets long and rotate them with a maintenance window.
  • Hash passwords and API tokens at rest; never return raw token material except once on creation.
  • Enable rate limiting on auth endpoints in production gateways.
  • Stream audit logs to your SIEM for sensitive environments.

Troubleshooting

Prisma client out of date: run pnpm prisma generate from the workspace root after schema edits.
  • 401 on admin API: verify tokens and clock skew; confirm NEXT_PUBLIC_API_URL matches the API origin.
  • Missing resource in UI: ensure the resource is exported in registry.ts and the user role includes the *.read permission.
  • Migration failures: inspect prisma/migrations, restore from backup, and never edit applied migration files.

Deployment

Deploy API, admin and web as separate services (Kubernetes, Fly.io, Railway, etc.) or as a single VM with process supervision. Run database migrations as a release phase hook before swapping traffic—same discipline as Nest.js apps using Prisma in production.

bash
pnpm --filter @openadminjs/api build
pnpm --filter @openadminjs/admin build
pnpm --filter @openadminjs/web build
node apps/api/dist/main.js
  • API — expose port 4000 (or behind TLS termination); set DATABASE_URL, JWT_SECRET, and CORS allowlists for admin/web origins.
  • Admin / Web — Next.js standalone output: bake NEXT_PUBLIC_API_URL and NEXT_PUBLIC_SITE_URL at build time per environment.
  • Migrations — run pnpm prisma migrate deploy (API package) in CI/CD before health checks go green.

Complete functionality reference

Use this section as a playbook for expected behavior. Canonical route tables live in HTTP API — full route map; this chapter highlights contracts, payload shapes, and guardrails.

SubsystemWhat it doesWhere it lives
Resource enginePrisma-backed CRUD + metadata from defineResourceapps/api/src/admin/admin-resource.service.ts, resources/*.resource.ts
Analytics shellDashboard metrics + chart fixtures + job healthadmin-analytics.service.tsGET /admin/resources/overview
Auth & realmsJWT issuance, refresh, logout, password/email flowsauth/auth.controller.ts, auth/auth.service.ts
StorefrontProducts, checkout, webhooks, shopper sessionstore/*.controller.ts, store/payment/*
Queue opsStats + manual dispatch testingqueue/queue.controller.ts
AI assistantChat, stream, conversations, artifactsadmin-ai.*, permission ai.chat
PluginsManifest CRUD + sandbox loadingadmin-plugins.*, plugins/bootstrap.ts
Mail transportPassword resets & transactional emailmail/mail.service.ts (internal only—no HTTP router)
Cross-cuttingCORS, helmet, validation pipes, PluginApiInterceptor, throttlerapp.module.ts, main.ts
Admin UINext.js App Router pages + resource shellapps/admin/app/*, components/app-shell.tsx
Public webSEO-facing marketing/resource pagesapps/web/*, lib/openadmin-client.ts

Authentication and session lifecycle

Login issues access and refresh tokens for the requested realm. Access tokens authorize requests; refresh tokens rotate sessions.

HTTP
POST /auth/login
Content-Type: application/json

{
  "email": "<superadmin-email>",
  "password": "<superadmin-password>",
  "realm": "admin"
}
JSON
{
  "accessToken": "<jwt>",
  "refreshToken": "<jwt>",
  "user": {
    "id": "clx123",
    "email": "<superadmin-email>",
    "permissions": ["*"]
  }
}
  • 401 for invalid/missing/expired token.
  • 403 for realm/permission mismatch.
  • Client should attempt one refresh, then redirect to login if refresh fails.

RBAC and permission enforcement

Permissions are enforced server-side for all CRUD and custom actions. UI visibility should mirror permissions, but security is API-driven.

TypeScript
permissions: {
  read: "posts.read",
  create: "posts.create",
  update: "posts.update",
  delete: "posts.delete"
},
actions: {
  publish: { label: "Publish", permission: "posts.publish" }
}

Resource CRUD API contract

Every resource automatically receives list/detail/create/update/delete routes and metadata endpoints.

HTTP
GET    /admin/resources/posts
GET    /admin/resources/posts/:id
POST   /admin/resources/posts
PATCH  /admin/resources/posts/:id
DELETE /admin/resources/posts/:id

Writes should be auditable and return deterministic error payloads for form-level error mapping.

Field metadata and UI behavior

The admin renders forms/tables from field metadata. This keeps behavior consistent without hand-written per-resource components.

TypeScript
fields: {
  title: { type: "text", required: true, list: true, searchable: true },
  status: { type: "select", options: ["draft", "published"], list: true, filterable: true },
  authorId: { type: "relation", resource: "users", displayField: "email" },
  secretKey: { type: "text", sensitive: true, list: false, detail: false }
}
  • relation fields use searchable pickers and foreign key values.
  • sensitive fields are stripped from sanitized outputs.
  • filterable/sortable/searchable control query features and UI controls.

Queries, search, filters, sorting

HTTP
GET /admin/resources/orders?page=1&limit=20&sort=createdAt:desc&search=enterprise
GET /admin/resources/orders?filter[status][eq]=paid
GET /admin/resources/orders?filter[total][gte]=10000
GET /admin/resources/orders?filter[currency][in]=USD,EUR

Unknown operators should return explicit validation errors (for example FILTER_OP_UNKNOWN) to prevent ambiguous behavior.

Custom actions and domain operations

Custom actions implement non-CRUD business transitions (publish, approve, cancel, regenerate, retry).

HTTP
POST /admin/resources/posts/clx123/actions/publish
Authorization: Bearer <token>
Content-Type: application/json

{ "note": "Editorial approval #42" }
  • Validate input payloads server-side.
  • Log action execution in audit trail.
  • Return structured error details for operator diagnostics.

Admin UX patterns (operations-ready)

  • Inline editing for fast field updates on detail pages.
  • Delete confirmations for destructive operations.
  • Action status feedback with clear success/error state.
  • Mobile mode should preserve core actions and avoid hidden destructive controls.

AI assistant configuration

AI provider setup is managed in admin settings and resolved by API service on each chat request. Access is gated by ai.chat permission.

JSON
{
  "activeProviderId": "openai",
  "providers": [
    { "id": "openai", "baseUrl": "https://api.openai.com/v1", "model": "gpt-4.1-mini" },
    { "id": "anthropic", "model": "claude-3-5-sonnet-latest" },
    { "id": "gemini", "model": "gemini-1.5-pro" }
  ],
  "systemPrompt": "Respond with plain text reply and optional structured action JSON."
}

AI streaming protocol (SSE)

JSON
{ "type": "delta", "delta": "Analyzing..." }
{ "type": "done", "reply": "Found 12 records.", "actionResult": { "count": 12 } }
{ "type": "error", "message": "Provider timeout", "code": "AI_STREAM_ERROR" }
  • delta: append progressive response text.
  • done: finalize reply and attach action results if present.
  • error: immediately stop loading and show diagnostics in UI.

AI actions, artifacts, and autofill

Assistant can generate files/images and save them as artifacts. Artifacts can be downloaded or injected into subsequent workflow steps.

JSON
{
  "action": {
    "type": "image",
    "prompt": "Generate minimal product hero image",
    "filename": "hero.png"
  }
}

Common refs: {{latest_image}}, {{latest_artifact}}, ai-artifact:<id>.

AI workflows and rollback diagnostics

JSON
{
  "action": {
    "type": "workflow",
    "transactionMode": "rollback_on_error",
    "steps": [
      { "type": "image", "prompt": "Create post cover" },
      { "type": "update", "resource": "posts", "id": "clx123", "data": { "coverImageId": "{{latest_image}}" } }
    ]
  }
}
  • best_effort: keep completed steps if later step fails.
  • rollback_on_error: attempt compensating actions.
  • Expose failedStep, partialResults, rolledBack, rollbackFailures.

Commerce and payment flow

HTTP
GET  /store/products
POST /store/checkout
POST /store/webhook/:provider
GET  /store/orders/:id/status
GET  /store/orders                # auth: public realm
GET  /store/session               # auth: public realm
POST /admin/resources/orders/:id/refund   # admin Ops (PaymentService)
  • Validate webhook signature before applying state changes.
  • Use idempotency for provider events.
  • Track payment transitions in transactions and audit logs.

Files, notifications, queue jobs

  • File assets — first-class files resource over the fileAsset model; uploads flow through the admin resource forms (no separate upload controller in core).
  • Notificationsnotifications resource plus list scoping for inbox-style experiences.
  • Job logs — persisted execution history with status columns; operational JSON from /jobs/stats / admin jobs page.

Production security checklist

  • Strong and rotated JWT secrets per environment.
  • Strict CORS allowlist for admin/web origins.
  • Least privilege role design and action permissions.
  • No provider tokens in client bundles.
  • Destructive AI actions require explicit confirmation.
  • Migrations run before traffic switch in deploy pipeline.

Partners & support

OpenAdminJS stays free thanks to contributors and partners. If your company wants to become a partner and be listed on the website, email openadminjs@proton.me.

  • Partner applications — branding, sponsorship and collaboration requests.
  • Financial support — for direct support options, contact openadminjs@proton.me.
  • Bug reports & feature requests — open an issue on GitHub; include a reproduction case and the output of pnpm exec openadminjs doctor.
  • Pull requests — all contributions welcome; check the CONTRIBUTING.md at the repo root before opening a PR.

Roadmap & next steps

The following capabilities are planned for upcoming releases. This list is intentionally public so contributors can pick up items early. Priorities shift based on community feedback — weigh in on GitHub Discussions.

Status key: 🟢 stable  |  🟡 in progress  |  ⚪ planned  |  💡 community request

Core platform

  • 🟢 Resource CRUD — list, create, edit, delete with field-level permissions
  • 🟢 RBAC — roles, permissions, realm-scoped tokens
  • 🟢 Audit logs — immutable per-resource action records
  • 🟢 JWT auth — access + refresh token flow with rotation
  • 🟢 Resource relations — picker UI for relation fields against other resources
  • 🟢 File uploads — local upload endpoint + storage abstraction via @openadminjs/storage
  • 🟢 Bulk actions — select multiple rows and apply a named action in one request
  • 🟢 Inline editing — double-click a cell in the list view to edit without opening the detail form

Admin UI

  • 🟢 Dark / light theme with system preference detection
  • 🟢 Responsive sidebar with mobile drawer
  • 🟢 Dashboard widgets — pluggable metric cards with real-time data
  • 🟢 Resource search — global ⌘K palette across all registered resources
  • 🟢 Rate-limit feedback — friendly “try again in Xs” messaging on HTTP 429 responses
  • 🟢 Global API error banner — unified ApiError feedback across admin pages
  • 🟢 Column ordering & density preferences — saved per user in localStorage
  • 🟢 Drag-and-drop for sortable resources (e.g. order integer field)
  • 🟢 Custom dashboard builder with drag-drop layout

i18n & localization

  • 🟢 Multilingual captions via LocalizedLabel maps on resources, fields and actions (i18n.locales pins fallbacks)
  • 🟢 Admin locale stored in localStorage, appended as ?locale=
  • 🟢 Locale picker in user settings (hidden from top bar by default; show via settings page)
  • 🟢 Pluralization + ICU — formatPlural(), formatIcuMessage() (MessageFormat) in @openadminjs/resource
  • 🟢 ICU message format support for complex translation strings

API & integrations

  • 🟢 OpenAPI / Swagger docs at /api/docs
  • 🟢 Webhook plugin — fire events after create/update/delete
  • 🟢 SSO bridge — OIDC Authorization Code + PKCE (/auth/sso/oidc/*), links users via User.oidcKey
  • 🟢 OIDC hardening — provider presets, stricter callback validation, richer error surfaces, callback test matrix
  • 🟢 Rate limiting — dual token buckets: IP (default) + per-user JWT (perUser, RATE_LIMIT_USER_PER_MIN)
  • 🟢 GraphQL — admin read queries at /graphql with pagination/search/sort and filter parity (adminResourceList, adminResourceRecord, bearer auth, realm admin)
  • 🟢 GraphQL parity tests — resolver filter mapping checks to stay aligned with REST query semantics

Developer experience

  • 🟢 CLI scaffold with npx openadminjs create
  • 🟢 openadminjs generate field — polished flags (--required, --list, --sortable, --filterable, --searchable, --label) + normalized field types + Prisma fragment output
  • 🟢 Storybook — pnpm --filter @openadminjs/admin storybook (example Logo story)

To propose a roadmap item, open a GitHub Discussion tagged roadmap. To pick up an in-progress or planned item, comment on the linked issue and a maintainer will assign it.