Setup your environnement

We made a docker file that builds Panora from sources, specifically to help you locally test your contributions. Here’s how to use it.

Execute these from your Panora’s repo root folder

Copy env variables

cp .env.example .env 

Rules for env variables

You must manually create a custom OAuth2 App inside the 3rd party and copy the critical values (likely client id and client secret)

You have the option to manage a custom 3rd party OAuth App.

Each custom 3rd party environment variable must be of the form PROVIDER_category_SOFTWAREMODE_ATTRIBUTE where

  • PROVIDER is any 3rd party name
  • category is for example [CRM, TICKETING, MARKETINGAUTOMATION, …]

Example :

  • HUBSPOT_CRM_CLOUD_CLIENT_ID = client_id_value_pasted_from_hubspot_developer_app
  • HUBSPOT_CRM_CLOUD_CLIENT_SECRET = client_secret_value_pasted_from_hubspot_developer_app

Remove previously installed dependencies

rm -rf node_modules .pnpm-store ./packages/api/dist ./packages/api/node_modules ./apps/webapp/node_modules ./apps/frontend_snippet/node_modules 
For Mac users only: echo -e "node-linker=hoisted\n package-import-method=clone-or-copy" > .npmrc

(Optional) Enable Grok

If you have to create an oAuth app for a provider and needs an https redirect uri you must enable Grok service and use your secure domain from them (it proxies requests to localhost:3000):

  • Add your auth token and domain from your Ngrok dashboard to the ngrok.yml file.
  • Uncomment the Grok service inside the file.

Start the Dockerfile

docker compose -f up

That’s all ! You can find the backend and other services running at their usual location. Editing code locally will immediately reflect.

Adding new Integrations ✨

Make sure you are inside packages/api/src where the server lives !

You want to add a new 3rd Party not yet supported ? 🧑‍🎤

Ie: Slack, Hubspot, Jira, Shopify …

First choose wisely which category the 3rd party belongs to among these:

  • crm
  • ticketing
  • accounting
  • ats
  • filestorage
  • hris
  • marketingautomation

You can find all categories inside packages/shared/src/categories.ts.

For the sake of the guide, now on we’ll consider adding a 3rd party belonging to the crm category.

Step 1: Ensure 3rd party metadata is set

Look into the packages/shared/src/connectors/metadata.ts file and check if the provider you want to build has its metadata set inside the CONNECTORS_METADATA object.

It should be available (if not contact us) with active field set to false meaning the integration has not been built.

Actually an integration is built in 2 parts :

  • the authentication part (oauth, api key, basic etc) which is built by the Panora team
  • the service integration where the mapping with our unified model is created whixch is what you’ll build in the next steps

Step 2: Build your provider service

You want to map a common object to your new 3rd Party ? 👩‍🎤

Ie: Contact, Ticket, Deal, Company …

For the sake of this guide, let’s map the common object contact under crm category to my3rdParty (in reality it would be a real 3rd party name).

An integration is considered valid when all common objects have been mapped. Then, after the PR is accepted we’ll be able to set active field to true inside CONNECTORS_METADATA


Add a new service to map your common object to your 3rd party

Create a new service folder with the name of your 3rd party. Let’s call it my3rdParty.

cd crm/contact/services/my3rdParty

You’ll now create 3 files :

  • index.ts where your service is created and direct interaction with your 3rd party API is handled
  • types.ts where the 3rd party specific API types are defined
  • mappers.ts where the mapping between our unified common model and the 3rd party one is handled
After copying the following code you’ll end up with linting/deps errors. It is fixed by our script at Step 2.

Create the index.ts file

It must implement the IContactService interface.

export interface IContactService {
    contactData: DesunifyReturnType,
    linkedUserId: string
  ): Promise<ApiResponse<OriginalContactOutput>>;

    linkedUserId: string
  ): Promise<ApiResponse<OriginalContactOutput[]>>;
export class My3rdPartyService implements IContactService {
    private prisma: PrismaService,
    private logger: LoggerService,
    private cryptoService: EncryptionService,
    private registry: ServiceRegistry,
  ) {
    this.logger.setContext( + ':' +,
    this.registry.registerService('my3rdParty', this);
  async addContact(
    contactData: 3rdPartyContactInput,
    linkedUserId: string,
  ): Promise<ApiResponse<3rdPartyContactOutput>> {}

  async syncContacts(
    linkedUserId: string,
  ): Promise<ApiResponse<3rdPartyContactOutput[]>> {}

Check other implementations under /crm/contacts/services to fill the core functions.


Create the types.ts file

The keen readers may have noticed 3rdPartyContactInput and 3rdPartyContactOutput. This is where types.ts comes in:
Go to the 3rd party API and insert the correct types asked by the API.

export interface 3rdPartyContact {
export type 3rdPartyContactInput = Partial<3rdPartyContact>;
export type 3rdPartyContactOutput = 3rdPartyContactInput;

Create the mappers.ts file

Last but not least, inside mappers.ts you have to build the mappings between our unified common object contact and your third party specific type 3rdPartyContact.

It must implement IContactMapper interface.

export interface IContactMapper {
    source: UnifiedContactInput,
    customFieldMappings?: {
      slug: string;
      remote_id: string;
  ): DesunifyReturnType;

    source: OriginalContactOutput | OriginalContactOutput[],
    customFieldMappings?: {
      slug: string;
      remote_id: string;
  ): Promise<UnifiedContactOutput | UnifiedContactOutput[]>;
export class My3rdPartyMapper implements IContactMapper {
    source: UnifiedContactInput,
    customFieldMappings?: {
      slug: string;
      remote_id: string;
  ): 3rdPartyContactInput {}

    source: 3rdPartyContactOutput | 3rdPartyContactOutput[],
    customFieldMappings?: {
      slug: string;
      remote_id: string;
  ): Promise<UnifiedContactOutput | UnifiedContactOutput[]> {}

Check other implementations under /crm/contacts/services to fill the core functions.


Enable your new service

After these 3 files are successfully created and filled, you are ready to fix all dependencies/linting issues that you may have.
To make sure the service is enabled, dependencies and imports must be added.
We built a script that does it in seconds. You can execute the given command from the root directory of Panora.

  docker build -t validate_connectors -f ./packages/api/Dockerfile.validate-connectors .
  docker run -v $(pwd):/app/ -e VERTICAL=crm -e OBJECT_TYPE=contact validate_connectors

The script will automatically scan the /crm/contact/services folder and detect any new service folder so all dependencies and imports are updated across the codebase.

Step 3: Test your new connector

Now you must test Panora unified endpoints with the new connector built.


Create a connection using the magic link

If the connector uses oAuth:
You need a set of credentials that you’ll set in .env if you use oAuth (check above how it works). Check this guide to create a connection through it. To activate your new connector on the magic link, visit this file : and change your connector status to active: true.

If the connector uses API KEY auth:
You currently have to do a POST request to create a connection.
Create your query encoded URI by computing this:

  const state = encodeURIComponent(JSON.stringify({ projectId, linkedUserId, providerName, vertical, returnUrl }));

The values for linkedUserId and projectId are to be found within the webapp at localhost

Insert an api key connection
curl --request POST \
     --url http://localhost:3000/connections/apikey/callback?state=state \
     --header 'Content-Type: application/json' \
     --data '{
        "apikey": "YOUR_CONNECTOR_API_KEY",
        // if your connector needs more parameters for auth (e.g username or whatever)
        //"username": "YOUR_USERNAME_VALUE"

Test with unified endpoints

Now after successfully creating a new connection, you can copy a connection token inside the webapp under http://localhost/connections. Create a Panora Api Key and make unified request with your connection token. Check this guide to do it !

Congrats, you now have built a new integration with Panora ! 🦸‍♀️

Nb: The development kit to add integrations out of the blue is coming soon 🎸