Connect: use integrations signed in as your users · triggerdotdev/trigger.dev · Discussion #441
When you're using integrations in the current version of Trigger.dev, you need to be the owner of the associated accounts. For example, you can use our GitHub integration using your GitHub personal access token or login to your account with OAuth in our dashboard.
This is great for a lot of use cases, but sometimes you want to be authenticated with services as your users. Some examples:
- Send Slack notifications to your users when events happen in your app
- Subscribe to changes in one of your users Airtable bases that syncs items to your platform
Basically you want to add integrations to your product.
What would the developer experience be?
Authenticating your users with APIs
- You would install one of our UI packages in your app (e.g. @trigger.dev/react)
- In your app you'd add a screen where you want your users to authenticate with an API
- When your user clicks "Airtable" you would invoke our UI, this would collect the required info and complete the authentication flow.
The exact structure of the React components is TBD.
import { AuthenticationFlow, AuthenticationTrigger, AuthenticationPanel, AuthenticationForm, } from "@trigger.dev/react"; function YourPage() { //this would come from your backend in real life const userId = "123"; return ( <div> <AirtableLogin userId={userId} /> </div> ); } //you would create this component in your app //this is a basic example, better to have a component that supports all integrations function AirtableLogin({ userId }: { userId: string }) { return ( <AuthenticationFlow integration="airtable" id="airtable" accountId={userId}> <AuthenticationTrigger className="rounded-sm bg-slate-200 p-2"> Sign in with Airtable </AuthenticationTrigger> <AuthenticationPanel className="border-slate-200 bg-white p-4"> <h1 className="text-2xl font-bold">Sign in with Airtable</h1> <AuthenticationForm className="flex flex-col gap-4" inputClassName="bg-white border border-slate-200 text-sm" labelClassName="text-slate-500 text-sm" /> </AuthenticationPanel> </AuthenticationFlow> ); }
For API keys this would show a form. OAuth would take them through the OAuth flow. The UI would be "headless", meaning it would have no styles so you can style it how you wish. We'll provide some examples to make it easier to get started. Some APIs require extra information, like Shopify which needs a Shop URL. We'll automatically render these in the form and collect the required data.
Writing Jobs
They would be very similar to normal Jobs.
When you declare an integration, you'd add the connect option:
const slack = new Slack({ //this can be anything you want, it's just a unique identifier id: "slack-customers", //this is new for Connect connect: true, });
When a Job is run the authentication details get injected immediately before run() is called. This is how it currently works for OAuth (as the underlying access tokens get refreshed so change over time). Currently for API keys, we never get sent them. But for integrations with connect: true we would store your user's API keys, like we do OAuth tokens. It's worth noting that all credentials are encrypted at rest, and all endpoints use HTTPS.
You write Jobs exactly like before. Integrations with connect enabled would be associated with the user's account. Also you'll be able to get the accountId inside the Job.
client.defineJob({ id: "slack-customer-notification", name: "Slack Customer Notification", version: "1.0.0", trigger: eventTrigger({ name: "slack.notification", schema: z.object({ channel: z.string(), message: z.string(), }), }), integrations: { slack, }, run: async (payload, io, ctx) => { //this will post a message to your user's Slack channel const message = await io.slack.postMessage("Slack message", { channel: payload.channel, text: payload.message, }); //details about the account are available in the context io.logger.log(`User id: ${ctx.account?.id}`); }, });
Triggering Jobs
This would work slightly differently, as runs need to be associated with a user.
eventTrigger (sendEvent)
This sendEvent would trigger the Job above, and cause a run as your user (with userId "123").
client.sendEvent( { name: "slack.notification", payload: { channel: "C01J9JZQZ9N", message: "Hello World", } }, { accountId: "123", } );
Webhooks
You'll need to register your user somewhere for the webhook. Then we'll subscribe to them for you.
//the job const github = new Github({ id: "github-customers", connect: true, }); const dynamicOnIssueOpenedTrigger = new DynamicTrigger(client, { id: "github-issue-opened", event: events.onIssueOpened, source: github.sources.repo, }); client.defineJob({ id: "listen-for-dynamic-trigger", name: "Listen for dynamic trigger", version: "0.1.1", trigger: dynamicOnIssueOpenedTrigger, integrations: { github }, run: async (payload, io, ctx) => { //do stuff }, });
Somewhere else in your code:
//the first param should be a unique ID for this particular registration of the Job await dynamicOnIssueOpenedTrigger.register(`${userId}-${repo}`, { owner, repo }, { accountId: "123", });
You could also register for this dynamic trigger in another job.
Using authenticated clients outside of Jobs
You'll be able to use the authenticated clients outside of Jobs. This is useful when you want to show UI in your app. In our example above, we need to allow the user to select their preferred Slack channel to receive notifications.
//some file in your backend, outside of a Job import { client } from "@/trigger"; import { WebClient } from "@slack/web-api"; //in real life the accountId would come from your app const auth = await client.getAuth({ id: "slack-customers", accountId: "123" }); //The Slack integration only supports OAuth if (auth.type !== "oauth2") throw new Error("Expected OAuth2 auth"); //create the normal Slack SDK using the access token const slackClient = new WebClient(auth.accessToken); const result = await client.conversations.list();
What changes to Trigger.dev are required to support this?
- The UI components need to be added to each of UI packages. We'd start with React.
- API endpoints need to be created to support the authentication flow and accept the data..
- Storing API Keys for Connect.
- Create a public getAuth method on the client.
- Integration's
cloneForRunmethod needs support for API keys from the backend. - Expanding on our dynamicTriggers to support subscribing to webhooks as your users.