Skip to main content

Platform Architecture

Jet Admin is a monorepo with three main workspaces:

jet-admin/
├── apps/
│ ├── backend/ Express + Prisma + PostgreSQL API (`PORT`, default 8090)
│ ├── frontend/ React 18 + Vite SPA
│ ├── mcp-server/ Standalone Express MCP bridge (`PORT`, default 5001)
│ └── admin/ Legacy Vite app (superseded by frontend)
├── packages/
│ ├── ui/ @jet-admin/ui — Button, Input, Dialog, PageHeader, …
│ ├── widgets-ui/ widget renderers (WIDGETS_MAP)
│ ├── datasources-logic/
│ ├── datasource-types/
│ ├── workflow-nodes / workflow-edges
│ └── …
└── docs/ this documentation site (Docusaurus)

Backend module pattern

Every feature lives in apps/backend/modules/<name>/ and follows the same layout:

FilePurpose
<name>.controller.jsThin request handlers: log, call service, respond
<name>.service.jsBusiness logic + Prisma queries
<name>.v1.routes.jsRoute definitions mounted under /api/v1/tenants/:tenantID/...
<name>.validator.jsZod schemas validated via utils/validation.utils
<name>.middleware.jsRequest enrichment (extract IDs for authorization)

Conventions:

  • Responses go through expressUtils.sendResponse(res, success, data, error) which spreads data into the top-level JSON body ({ success, ...data }).
  • Authorization uses Casbin policies derived from config/permissions.json. Routes call authMiddleware.authorize(P.<resource>.<action>); cross-resource checks attach extracted IDs to the request via { ...P.x.y, reqKey: "xIDs", skipIfMissing: true }.
  • Creator access: after creating a resource, services call grantCreatorAccess(tenantID, resourceType, resourceID, authContext, userID) from config/casbin.config.
  • Database models are in prisma/schema.prisma, all named tbl*, primary keys are UUIDs generated by gen_random_uuid().
  • Secrets live in vault (tblVaultCredentials) or encrypted datasourceOptions; they must never appear in logs, exports or API responses (utils/sensitive.js).

All entity routers are nested inside modules/tenant/tenant.v1.routes.js, which also mounts the cross-cutting routers:

Mount pointRouterPurpose
/:tenantID/app-pagesappPageApp page CRUD + versions
/:tenantID/queriesdataQueryData query CRUD + execution
/:tenantID/widgetswidgetWidget CRUD + file upload
/:tenantID/workflowsworkflowWorkflow CRUD + engine (+ /data-collection)
/:tenantID/datasourcesdatasourceConnections + proxy + test
/:tenantID/listenerslistenerEvent listeners + actions
/:tenantID/cronjobscronJobScheduled jobs + history
/:tenantID/usersuserManagementTenant members
/:tenantID/rolestenantRoleCustom roles + policy sync
/:tenantID/apikeysapiKeyAPI keys (+ clone)
/:tenantID/auditauditAudit log list + CSV export
/:tenantID/importbundleExport/import preview + execute
/:tenantID/foldersfolderFolder organization + bulk move
/:tenantID/widget-librarywidgetLibraryLibrary preview/install (tenant side)

Top-level (outside the tenant router, see apps/backend/index.js):

Mount pointPurpose
GET /healthUnauthenticated health probe ({status:'ok', timestamp}); Docker/Render healthchecks target this
/api/v1/authFirebase session config endpoints
/api/v1/operator/auth, /api/v1/operatorOperator realm (platform admins; PBKDF2 + opaque sessions; no Casbin). Widget-library publish/unpublish lives here (GET|POST /roles, /permissions, /widget-library)
/api/v1/tenants/:tenantID/aiJet Agent chat streaming (POST /chat/stream, DELETE /session)
/api/v1/oauthGoogle OAuth (/google/auth/:tenantID, /google/callback)
/webhooksDatasource webhook ingress (/v1/inbound/:tenantID/:pathSuffix, /v1/inbound/:listenerID; open CORS)
warning

Earlier drafts placed the shared widget library registry at GET /api/v1/widget-library. That route does not exist — the registry is managed through the operator router (/api/v1/operator/widget-library), and tenants install through /:tenantID/widget-library. Corrected here against apps/backend/index.js and modules/tenant/tenant.v1.routes.js.

Reference graph between entities

AppPage ──> Widget (appPageConfig.widgets = ["widget_<widgetID>_<suffix>", …])
AppPage ──> DataQuery (appPageConfig.dataSources[].queryID)
AppPage ──> Workflow (appPageConfig.dataSources[].workflowID)
AppPage ──> Listener (appPageConfig.dataSources[].listenerID)
Widget ──> DataQuery (widgetConfig event actions TRIGGER_QUERY.queryID)
Widget ──> Workflow (widgetConfig event actions TRIGGER_WORKFLOW.workflowID)
Workflow ─> DataQuery (workflow node nodeType="dataQuery", nodeConfig.dataQueryID)
DataQuery -> Datasource (datasourceID column)
Listener ─> Datasource (datasourceID column, NOT NULL)
Listener ─> Workflow/Query (listener actions actionConfig.workflowID / .dataQueryID)

This graph drives export/import bundles, install previews and dependency warnings.