@saasquatch/integration-boilerplate-node
v2.4.0
Published
Node/Express boilerplate for building SaaSquatch integrations
Downloads
72
Maintainers
Keywords
Readme
SaaSquatch is a platform for managing referral and rewards programs for digital businesses. Integrations with SaaSquatch are typically built as microservices which respond to webhooks and form handler triggers from SaaSquatch's forms platform.
This boilerplate allows you to very quickly stand up a Node/Express microservice in TypeScript which can act as a SaaSquatch integration. It provides easy ways to do the easy things, and hooks for adding advanced configuration to make the more advanced things possible when building your integration.
Getting Started
Here's an example of the most basic integration that handles SaaSquatch webhooks:
import { createIntegrationService } from "@saasquatch/integration-boilerplate-node";
async function main() {
const service = await createIntegrationService({
handlers: {
async webhookHandler(service, webhook, _config, _graphql, res) {
service.logger.info("Handling a webhook! %o", webhook);
res.sendStatus(200);
},
},
});
service.run();
}
main();
Concepts
Authentication
SaaSquatch integrations are authenticated in two primary ways:
- As an OAuth application which provides access to SaaSquatch APIs from your integration. This requires an Auth0 application configured in SaaSquatch's backend for your integration.
- With a tenant-scoped token provided to your integration's frontend on the Integrations page of the SaaSquatch portal and in the form configuration contexts for initial data and submit actions.
We are working on making it easier for customers to build their own integrations, however for now only SaaSquatch's integration team is able to properly configure the necessary resources for authenticating an integration.
Configuration
There are three different types of configuration relevant to an integration. They can be provided as type parameters to
createIntegrationService
in order to customize them for your integration:
createIntegrationService<ServiceConfig, IntegrationConfig, FormConfig>();
Service configuration
Service configuration parameters are configured with environment variables, which are typed through the use of the
typed-config
package. There are a number of built-in service configuration
parameters common to all integrations. These are:
| Environment Variable | Property | Default | Description | | ---------------------------------------------- | ----------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ | | SERVICE_NAME | serviceName | | The name of the integration, in kebab case. For example: "my-cool-integration" | | PORT | port | 10000 | The port on which to run the microservice | | SERVER_LOG_LEVEL | serverLogLevel | info | The log level for the default logger | | SERVER_LOG_FILE | serverLogFile | | The optional path to write server logs to | | ENFORCE_HTTPS | enforceHttps | true | Enforce HTTPS on the Express server | | PROXY_FRONTEND | proxyFrontend | | Proxy the integration frontend through the Express server, specify a URL like http://localhost:3000 | | STATIC_FRONTEND_PATH | staticFrontendPath | .../../frontend/build | The location (relative to your main module) of the integration's frontend (ignored if PROXY_FRONTEND is set) | | STATIC_FRONTEND_INDEX | staticFrontendIndex | index.html | The root file of your integration frontend (ignored if PROXY_FRONTEND is set) | | SAASQUATCH_APP_DOMAIN | saasquatchAppDomain | app.referralsaasquatch.com | The domain of the SaaSquatch core application | | SAASQUATCH_AUTH0_DOMAIN | saasquatchAuth0Domain | | The Auth0 domain for OAuth authentication to the SaaSquatch API | | SAASQUATCH_AUTH0_CLIENT_ID | saasquatchAuth0ClientId | | The Auth0 client ID for OAuth authentication to the SaaSquatch API | | SAASQUATCH_AUTH0_SECRET | saasquatchAuth0Secret | | The Auth0 client secret for OAuth authentication to the SaaSquatch API | | GOOGLE_WORKLOAD_IDENTITY_PROJECT_ID | googleWorkloadIdentityProjectId | | The project ID for Google Workload Identity Federation | | GOOGLE_WORKLOAD_IDENTITY_POOL | googleWorkloadIdentityPool | | The Google Workload Identity Federation pool | | GOOGLE_WORKLOAD_IDENTITY_PROVIDER | googleWorkloadIdentityProvider | | The Google Workload Identity Federation proivder | | GOOGLE_WORKLOAD_IDENTITY_SERVICE_ACCOUNT_EMAIL | googleWorkloadIdentityServiceAccountEmail | | The email address of the Google service account to request credentials for via Workload Identity Federation |
The authentication parameters SAASQUATCH_AUTH0_*
are required, and do not have defaults. Your service will not start
without them.
Service configuration can be customized by extending BaseConfig
using the primitives of typed-config
:
import { key, optional } from "typed-config";
import {
createIntegrationService,
BaseConfig,
asBoolean,
} from "@saasquatch/integration-boilerplate-node";
class MyCustomConfig extends BaseConfig {
@key("REQUIRED_CUSTOM_SETTING")
public requiredCustomSetting!: string;
@key("OPTIONAL_CUSTOM_SETTING", asBoolean)
@optional("default_value")
public optionalCustomSetting!: asBoolean;
}
async function main() {
const service = await createIntegrationService({
configClass: MyCustomConfig,
});
// The config of the service is always available at service.config
service.logger.debug(service.config.requiredCustomSetting);
}
main();
NOTE: At the the time of writing the asBoolean
and asNumber
transformers from typed-config
were broken, so this
package exports versions that work correctly. See this issue.
Integration configuration
Integration configuration is saved by your integration's configuration frontend and stored with the tenant in SaaSquatch. These parameters are the tenant's specific configuration for the integration, and are made available automatically to webhook and form handlers.
Form handler configuration
Form handler configuration is configuration specific to a particular form, and is configured by your integration's configuration frontend when called in a form's configuration context (either to configure initial data actions, or submit actions). The form handler configuration is available in the form handler context passed to a form handler function.
Handlers
A number of "on-rails" handler function definitions are available making simple integrations easy to develop.
Webhook handler
A webhook handler takes the following arguments:
| Argument | Description |
| --------- | -------------------------------------------------------------------------------------------------------------- |
| service
| The integration service, which gives you access to service config, and the built-in logger |
| webhook
| The body of the webhook. SaaSquatch webhooks contain a type
property to determine what kind of webhook it is |
| config
| The integration configuration for the tenant for which the webhook was sent |
| graphql
| A tenant-scoped GraphQL function for performing queries against SaaSquatch's GraphQL API |
| res
| The Express Response object |
For webhooks to be considered successful by SaaSquatch, it must return a 200 HTTP status. If you define a webhook handler, you are responsible for sending the response status.
Webhook handlers do not need to return a body.
Form handlers
There are 4 types of form handler: formSubmitHandler
, formValidateHandler
, formIntrospectionHandler
,
formInitialDataHandler
called at different times in a form's lifecycle. They all take the following arguments:
| Argument | Description |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| service
| The integration service, which gives you access to service config, and the built-in logger |
| context
| The form context, see the schema definition for the properties it contains |
| graphql
| A tenant-scoped GraphQL function for performing queries against SaaSquatch's GraphQL API |
The expected response body of a form handler depends on the type, and the expected responses can be found in the schema:
- FormHandlerSubmitResponseBody
- FormHandlerValidateResponseBody
- FormHandlerIntrospectionResponseBody
- FormHandlerInitialDataResponseBody
Form handlers can also return an error response, which is defined in FormHandlerErrorResponseBody.
The Typescript types are based on these schemas and should help you make sure your handlers are returning the right kind of data.
Introspection handler
An integration introspection handler takes the following arguments:
| Argument | Description |
| --------------------------- | ------------------------------------------------------------------------------------------ |
| service
| The integration service, which gives you access to service config, and the built-in logger |
| config
| The integration configuration for the tenant for which the webhook was sent |
| templateIntegrationConfig
| The system-wide integration config stored in CMS |
| tenantAlias
| The tenant alias of the tenant for which the introspection is taking place |
For successful introspection, the integration should modify the
templateIntegrationConfig
as necessary and return it in the JSON body under the key
templateIntegrationConfig
.
Advanced Use Cases
Custom routing
Many integrations can get away with only providing handlers for webhooks and forms, however more complex integrations that may respond to webhooks from 3rd party systems need to define their own routing and middleware.
There is a router available on the service as service.router
for this purpose, which can be configured with routes
and middleware as needed.
import { createIntegrationService } from "@saasquatch/integration-boilerplate-node";
async function main() {
const service = await createIntegrationService();
service.router.get("/myCustomRoute", (req, res) => {
res.send("Hello World");
});
service.run();
}
main();
NOTE: There are two reserved routes for the built-in handlers, /form
and /webhook
. Don't override these if you
want them to work.
Custom routes called from Integrations page
Within the Integrations page in the SaaSquatch portal, your integration's configuration interface is provided with a
tenant scoped token. For routes that are called by your configuration interface, there is a special middleware which
will validate the token and place the tenantAlias
directly on the request for you. This makes it easier to implement
things like OAuth flows with 3rd party services or use your integration to securely query GraphQL for data required by your
integration's frontend (like a list of programs, for example).
Integration configuration frontends are loaded in iframes and use Penpal for communication with the SaaSquatch portal. How to easily build an integration frontend is the concern of integration-boilerplate-react.
In your configuration interface you could do this:
fetch("/called-from-config-ui", {
headers: { Authorization: `Bearer ${penpal.tenantScopedToken}` },
});
And respond to it in the integration like this (see the section on GraphQL below to understand the call to
service.getTenant
):
import { gql } from "@saasquatch/integration-boilerplate-node";
router.get(
"/called-from-config-ui",
service.tenantScopedTokenMiddleware, // This is the middleware you should use
async (req, res) => {
// The tenantAlias is on the req object
const { graphql } = await service.getTenant(req.tenantAlias!);
const response = await graphql(gql`query { ... }`);
res.sendStatus(200);
}
);
GraphQL
In your custom routes, you may need to make GraphQL queries based on data coming in from 3rd party services. The first thing to say is make sure you are appropriately authenticating incoming requests from 3rd parties!
To get the integration config for the tenant and a tenant-scoped GraphQL function, you can call getTenant
on the
service passing the tenant alias, which will return to you the integration config for the tenant and a tenant-scoped GraphQL function.
NOTE: If happen to pass a tenant alias for a tenant that does not have your integration enabled, then getTenant
will fail.
Here's an example:
import { Router } from "express";
import {
createIntegrationService,
gql,
} from "@saasquatch/integration-boilerplate-node";
async function main() {
const router = express.Router();
const service = await createIntegrationService({
handlers: {
webhookHandler(service, webhook, config, graphql, res) {
service.logger.info("Handling a webhook! %o", webhook);
res.sendStatus(200);
},
},
customRouter: router,
});
router.get("/some3rdPartyWebhook/:tenantAlias", async (req, res) => {
// Ensure that the webhook is legitimate - implement something like this, or a middleware for this route
// to validate the webhook is coming from the service you expect
validate3rdPartyWebhook();
// config - the tenant's integration config
// graphql - a tenant-scoped GraphQL function
const { config, graphql } = await service.getTenant(req.params.tenantAlias);
const response = await graphql(gql`query { ... }`);
res.sendStatus(200);
});
service.run();
}
main();
The graphql
function can be typed with the response, i.e graphql<MyResponseType>(...)
and takes optional variables
and operationName
arguments.
Google Workload Identity Federation
If your service requires access to Google services, typically you would generate a Service Account Key in JSON format, and place it within your environment as GOOGLE_APPLICATION_CREDENTIALS. Authorization is then controlled by the IAM permissions assigned to the service account.
Service account keys are powerful, and require regular rotation to prevent them from being compromised. An alternative to service account keys is Workload Identity Federation, which can bind an identity from an OpenID Connect Identity Provider (such as Auth0) to a service account in GCP.
As we already have Auth0 credentials for each service, it makes sense to use this method.
Configuring Workload Identity Federation is described in the Google documentation: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers
In short, to successfully authorize access to Google services via Auth0 there are a number of configuration setups required:
- Configure a pool and provider for Workload Identity Federation
- Configure an API and machine-to-machine access for your Auth0 application
- Provide the Workload Identity configuration options to the service to automatically configure Google libraries
Configure Workload Identity Federation
Create a pool in Workload Identity Federation and add a new provider. The provider's Issuer URL should be your Auth0's tenant URL e.g. https://<tenant>.auth0.com/
.
Under attribute mappings, you want to configure add at least the following:
google.subject
->assertion.sub
attribute.iss
->assertion.iss
To limit the identities allowed to just your Auth0 application, add an attribute condition that checks the issuer and the subject (replacing your Auth0 tenant domain and the client ID of your application):
assertion.iss == 'https://<tenant>.auth0.com/' && assertion.sub == '<clientId>@clients'
Then, grant access to your service account to be used by the identities in the pool.
Configure Auth0
In Auth0, we need to configure a new API so that we can generate tokens with the subject of the identity pool. Add a new API with a URL like https://iam.googleapis.com/projects/<projectNumber>/locations/global/workloadIdentityPools/<pool>/providers/<provider>
.
Make sure to enable your Auth0 application for machine-to-machine access on this API.
Add service options
In your environment configure the following environment variables to automatically enable access to Google services via Workload Identity Federation:
GOOGLE_WORKLOAD_IDENTITY_PROJECT_ID
GOOGLE_WORKLOAD_IDENTITY_POOL
GOOGLE_WORKLOAD_IDENTITY_PROVIDER
GOOGLE_WORKLOAD_IDENTITY_SERVICE_ACCOUNT_EMAIL