Skip to content

Design Hub developer guide - company support plugins

Design Hub's company supports plugins are a collection of API endpoints that facilitate integrating the application into an organization's project management, authorization and enterprise resource planning systems. As such it handles concepts other than the rest of the plugin types.

A company plugin is a NodeJS module, stored in a folder set by Design Hub's configuration file (servicesDirectory).

NodeJS module API

Company support plugins are NodeJS modules, denoted by their filename: *.company.js and location in the services directory as configured during installation.

A company support plugin exports the following properties:

Name Type Required Description
name string yes Unique identifier of the plugin, used by Design Hub for identification and internal communication. If multiple plugins use the same identifier, the last one to be loaded overrides the others.
label string yes Human readable name of the plugin, used by Design Hub to display GUI elements related to this plugin.
getProjects async function no Called automatically during initialization and then on a predefined daily schedule to obtain an overall list of projects to be used by the application.

Arguments: none
this includes domain and dhUtils

Return value: Promise of array of objects. Each object corresponds to a project. The objects in the array should contain name and label attributes for a project. Label will be used on the GUI.
getProjectsWithAccess async function no Access-level-aware alternative to getProjects, called on the same schedule. When implemented, Design Hub switches the domain to explicit read/write project access levels instead of the assigned/non-assigned model. See Configuration guide - Project access levels.

Arguments: none
this includes domain and dhUtils

Return value: Promise of array of objects. Each object corresponds to a project and should contain name, label, and a membersByAccess array of {username, accessLevel} entries, where accessLevel is "read" or "write".
getUserProjects async function no Called automatically during the successful authentication of a user, to obtain project assignments. The function must return a Promise of the results. The results are stored in the application database and used for authorization.

Arguments:
user (object) A javascript object describing the calling user
this includes domain and dhUtils for the current call

Return value: Promise of array of objects. Each object corresponds to a project. The objects in the array should contain name and label attributes for a project.
getUserProjectsWithAccess async function no Access-level-aware alternative to getUserProjects, called on the same successful-authentication trigger. When implemented, the returned access levels are stored and used for authorization instead of the assigned/non-assigned model. See Configuration guide - Project access levels.

Arguments:
user (object) A javascript object describing the calling user
this includes domain and dhUtils for the current call

Return value: Promise of array of objects. Each object corresponds to a project and should contain name, label, and a projectAccess attribute of "read" or "write".
getUserGroups async function no Called automatically during the successful authentication of a user, to obtain group memberships. The function must return a Promise of the results. The results are stored in the application database and used for authorization.

Arguments:
user (object) A javascript object describing the calling user
this includes domain and dhUtils for the current call

Return value: Promise of array of string. Each string corresponds to a group's name.
domains array of strings yes List of domains where this plugin may be used, when authentication is enabled in Design Hub. Use * to allow any domain.
onConfigurationChanged function no A callback function that Design Hub calls during initialization and whenever an administrator updates the Secrets of the system.

Arguments:
config (Object) An object with secrets attribute containing the key-value pairs of secrets from the Admin interface
 **_NOTE:_** if a plugin implements either `getProjectsWithAccess` or `getUserProjectsWithAccess`, Design Hub automatically switches the domain to explicit project access level mode; users without recorded access data can still log in, but they won't have access to any project's content until it is provided. See [Configuration guide - Project access levels](design-hub_configuration-guide.md#project-access-levels) for details.

NOTE: both endpoints are declarative, not incremental — each call replaces the previously stored access levels for the users it covers, it does not merge with them.

  • getUserProjectsWithAccess is called once per user, at that user's login. Any project the user previously had access to that's missing from this response is removed. Returning an empty array removes all of that user's access levels, not none.
  • getProjectsWithAccess is called on the daily refresh (and at startup). For every user who appears in at least one project's membersByAccess, their access is fully replaced the same way. A user who is entirely absent from the response is left untouched — the sync only runs for users it finds in the response, so it won't proactively revoke access from someone who simply isn't mentioned.

Example: sourcing project access levels from a JSON file

project-access.json is not a Design Hub-recognized file — Design Hub only calls getProjectsWithAccess/getUserProjectsWithAccess and reads their return values; it has no knowledge of where a plugin sources that data. The example below is one way to implement it, backed by a plain JSON file instead of a database or external API.

Since it's read by your own plugin code (not by Design Hub), the file must be placed somewhere your plugin process can read it — in practice, that means inside servicesDirectory (see Plugin configuration), typically right next to the plugin module itself so it can be located with path.join(__dirname, ...).

project-access.json (placed alongside the plugin file in servicesDirectory):

{
  "projects": [
    {
      "name": "project-key",
      "label": "Project Display Name",
      "id": 1001,
      "members": [
        { "username": "jsmith", "accessLevel": "write" },
        { "username": "jdoe", "accessLevel": "read" }
      ]
    }
  ]
}

project-access.company.js:

"use strict";
const fs = require("fs");
const path = require("path");

const DATA_FILE = path.join(__dirname, "project-access.json");

const readProjectData = () => JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));

/**
 * @typedef ProjectMemberAccess
 * @prop {string} username
 * @prop {"read"|"write"} accessLevel
 *
 * @typedef ProjectWithMembersAccess
 * @prop {string} name
 * @prop {string} label
 * @prop {number} id
 * @prop {ProjectMemberAccess[]} membersByAccess
 *
 * @typedef UserProjectAccessInfo
 * @prop {string} name
 * @prop {string} label
 * @prop {number} id
 * @prop {"read"|"write"} projectAccess
 */

/**
 * Bulk endpoint: called on initialization and the daily project refresh.
 * @returns {Promise<ProjectWithMembersAccess[]>}
 */
async function getProjectsWithAccess() {
    const data = readProjectData();
    return data.projects.map((project) => ({
        name: project.name,
        label: project.label,
        id: project.id,
        membersByAccess: project.members.map((member) => ({
            username: member.username,
            accessLevel: member.accessLevel
        }))
    }));
}

/**
 * Per-user endpoint: called on successful authentication.
 * @param {{userName: string}} user
 * @returns {Promise<ProjectWithMembersAccess[]>}
 */
async function getUserProjectsWithAccess(user) {
    const data = readProjectData();
    return data.projects
        .filter((project) => project.members.some((member) => member.username === user.userName))
        .map((project) => {
            const member = project.members.find((member) => member.username === user.userName);
            return {
                name: project.name,
                label: project.label,
                id: project.id,
                projectAccess: member.accessLevel
            };
        });
}

module.exports = {
    name: "project-access-example",
    label: "Project Access Example",
    domains: ["*"],
    getProjectsWithAccess,
    getUserProjectsWithAccess
};
 **_NOTE:_** the field names in `project-access.json` (`members`, `accessLevel`) are this example's own choice, not a Design Hub requirement — only the plugin's return values (`membersByAccess` / `projectAccess`, as documented in the table above) are actually read by Design Hub.

Plugin Skeleton

skeleton.company.js

//@ts-check
"use strict";

/**
 * @typedef User
 * @prop {string} userName
 * @prop {TokenSet} [tokenSubset]
 * 
 * @typedef TokenSet
 * @prop {string} [access_token]
 * @prop {string} [token_type]
 * @prop {string} [id_token]
 * @prop {string} [refresh_token]
 * @prop {number} [expires_in]
 * @prop {number} [expires_at]
 * @prop {string} [session_state]
 * @prop {string} [scope]
 * 
 * @typedef GetCompanyContext
 * @prop {string} domain
 * @prop {DhUtils} dhUtils
 *
 * @typedef {Object} DhUtils Utility functions. See API of `this.dhUtils` in the plugins guide.
 * @prop {(data: ReqOptions) => Promise<any>} get HTTP GET request
 * @prop {(data: ReqOptions) => Promise<any>} post HTTP POST request
 * @prop {(data: ReqOptions) => Promise<any>} req HTTP request with configurable method
 * @prop {(structure: string, toFormat: string) => Promise<string>} convert
 * @prop {(fileContents: string|Buffer, toFormat: string) => Promise<string>} convertFile
 * @prop {(structures: string[], toFormat: string) => Promise<{structure: string, successful: boolean, error?: string}[]>} batchConvert
 * @prop {(structures: string[], toFormat: string) => Promise<string>} append
 * @prop {(structure: string, dimension?: 2|3, format?: string) => Promise<string>} clean
 * @prop {(structures: string[], width?: number, height?: number) => Promise<string[]>} getImageURLs
 * @prop {PDBUtilities} PDBUtilities
 *
 * @typedef {Object} PDBUtilities
 * @prop {(pdbCode: string) => Promise<string>} load
 * @prop {(pdbCode: string) => Promise<string[]>} findLigands
 * @prop {(pdbFile: string) => string[]} findResidues
 * @prop {(pdbCode: string, ligandCode: string) => Promise<string|false>} getResidue
 * @prop {(pdbFile: string, ligandCode: string) => Promise<string|false>} getResidueFromFile
 *
 * @typedef {Object} ReqOptions HTTP request parameters. See API of `req` in the plugins guide.
 * @prop {string|URL} url
 * @prop {'GET'|'POST'|'PUT'|'PATCH'|'DELETE'} [method]
 * @prop {Object<string, string>} [headers]
 * @prop {any} [body]
 * @prop {boolean} [json]
 *
 * @typedef Project
 * @prop {string} name
 * @prop {string} label
 * 
 * @typedef ConfigurationValues
 * @prop {{[key: string]: string}} secrets
 */


/**
 * Find project membership of user
 * @this {GetCompanyContext}
 * @param {User} user
 * @returns {Promise<Project[]>}
 */
async function getUserProjects(user) {
    if (!user?.tokenSubset?.access_token) {
        console.error("getUserProjects - missing access token for user", user);
        throw new Error("getUserProjects access token not found for user");
    }

  //load projects of user

    return [{
        name: "p1",
        label: "Project 1"
    }];
}

/**
 * Find group/roles of user
 * @this {GetCompanyContext}
 * @param {User} user
 * @returns {Promise<string[]>}
 */
async function getUserGroups(user) {
    if (!user?.tokenSubset?.access_token) {
        console.error("getUserGroups - missing access token for user", user);
        throw new Error("getUserGroups access token not found for user");
    }

    //load groups of user

    return ["USER"];
}


/**
 * Load complete project dictionary
 * @this {GetCompanyContext}
 * @return {Promise<Project[]>}
 */
async function getProjects() {
    return [{
        name: "p1",
        label: "Project 1"
    }];
}


/**
 * Store and use values provided by Admin interface's Secret manager
 * @param {ConfigurationValues} config
 */
function onConfigurationChanged(config) {
  console.log("plugin-name configuration", config.secrets);
}

module.exports = {
    name: "skeleton",
    label: "Skeleton Company",
    getUserProjects: getUserProjects,
    getUserGroups: getUserGroups,
    getProjects: getProjects,
    onConfigurationChanged: onConfigurationChanged,
    domains: ["*"]
};