Skip to main content

External Functions

We love to extol the virtues of deterministic functions but sometimes you just need to do something non-deterministic. The most common scenario where non-determinism is required is when interacting with an external third-party, such as processing a payment on Stripe.

Convex makes it easy to run general-purpose third-party serverless functions that can communicate with the Convex deployment. In this tutorial we'll extend our chat app to call a function running on a hosting provider such as Netlify or Vercel, fetch an animated GIF from GIPHY, and then write the URL into a chat message on Convex. Because our chat app requires authentication to send messages, we'll also forward the ID token to the hosted function so it can send the message as the user.

(Technically you could implement this GIF functionality client-side but the serverless function allows us to hide the GIPHY access key from the client and restrict what can be posted.)

This example builds on the hosted version of the chat app from the Hosting tutorial. The steps below include information for both Netlify and Vercel, but should be applicable to any hosting provider supporting functions. If you need any help the completed code is in the 4a-external-functions-netlify and 4b-external-functions-vercel directories in the Convex Demos repo.

Local environment

Both Netlify and Vercel support local development environments for testing out your local functions. As a first step, let's set up the local environment:

npm install -g netlify-cli

We'll assume you already have an active Convex project via npx convex init and have pushed the Convex functions with npm convex push. Ordinarily if you wanted to run the web client locally you'd run npm run dev but now you can run the client within a simulated hosted environment instead by running:

netlify dev

Make sure you can run this command and load the chat app. It should behave just as before since we haven't added any external functions yet.

tip

If this is your first time using the CLI tool, it may require some initial setup to link it with the deployment created in the previous section. Either tool will guide you through the setup as needed.

Hosted functions that we add will get executed locally within the respective dev environment. The Convex functions will run on the deployment specified in convex.json and any chat messages will be visible to clients on the production chat deployment. Later tutorials will cover information on how to test Convex functions on a non-production deployment.

Your first external function

Let's start with the Netlify Hello World TypeScript tutorial. Import the Netlify functions module:

npm install @netlify/functions

and create the following source file in the Netlify functions directory netlify/functions/:

netlify/functions/hello.ts
import { Handler } from "@netlify/functions";

const handler: Handler = async (event, context) => {
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello World" }),
};
};

export { handler };

Next, we need to call this function from the chat app. This can just be a simple http fetch. Add the following code somewhere in App.tsx:

fetch("/.netlify/functions/hello", { headers: { Accept: "application/json" } })
.then(response => response.json())
.then(body => console.log(body))
.catch(error => console.log(error));

Reload your chat app and ensure that {message: 'Hello World'} is showing up in your browser JavaScript console.

Convex and the outside world

We're successfully calling a hosted function from our chat app but now it's time to level-up and talk to both Convex and a third-party service within a function.

Our desired end-user functionality is to be able to type a chat message like /giphy wombat and have an animated GIF of a wombat show up in the chat stream. We're going to need some help from GIPHY for this. Create a developer account and a free API app key on the developer dashboard.

The desired handler function should look something like the following:

netlify/functions/post-gif.ts
import { Handler } from "@netlify/functions";
import { ConvexHttpClient } from "convex/browser";
import fetch from "node-fetch";
import convexConfig from "../../convex.json";
import { Id } from "../../convex/_generated/dataModel";

const convex = new ConvexHttpClient(convexConfig.origin);

// Replace this with your own GIPHY key obtained at
// https://developers.giphy.com/ -> Create Account.
const GIPHY_KEY = "QrXTp0FioARhBHalPs2tpA4RNOTLhFYs";

function giphyUrl(query: string) {
return (
"https://api.giphy.com/v1/gifs/translate?api_key=" +
GIPHY_KEY +
"&s=" +
encodeURIComponent(query)
);
}

interface GiphyResponse {
data: { embed_url: string };
}

// Post a GIF chat message corresponding to the query string.
const handler: Handler = async (event, context) => {
const params = JSON.parse(event.body!);
const channelId = new Id("channels", params.channel);
const token = params.token;
convex.setAuth(token);

// Fetch GIF url from GIPHY.
const gif = await fetch(giphyUrl(params.query))
.then(response => response.json() as Promise<GiphyResponse>)
.then(json => json.data.embed_url);

// Write GIF url to Convex.
await convex.mutation("sendMessage")(channelId, "giphy", gif);

return {
statusCode: 200,
body: JSON.stringify(gif),
};
};

export { handler };

The first thing this code does is create a ConvexHttpClient. This is a non-reactive client that can run Convex queries and mutations. It's a simple one-line call to write a chat message into Convex inside the handler.

The handler itself is pretty basic. It reads the chat channelId and user's token passed in as parameters in the request body, along with a query string we'll use to look up a GIF. The token is passed to the Convex client so the mutation will be authenticated, then it makes an external call to GIPHY to fetch the URL of a relevant GIF. Finally, it writes that URL to Convex as a chat message.

You may notice that we've added an extra parameter to the sendMessage Convex function to include the message type. More on that next.

Wiring it up

Time to wire everything together and start posting GIFs. First we'll update convex/schema.ts to add a format field to the messages table:

convex/schema.ts
messages: defineTable({
body: s.string(),
channel: s.id("channels"),
format: s.string(), // "text" or "giphy"
user: s.id("users"),
}),

and update sendMessage.ts to handle this new field:

convex/sendMessage.ts
import { Id } from "./_generated/dataModel";
import { mutation } from "./_generated/server";

// Send a message to the given chat channel.
export default mutation(
async (
{ db, auth },
channel: Id<"channels">,
format: string,
body: string
) => {
const identity = await auth.getUserIdentity();
if (!identity) {
throw new Error("Unauthenticated call to sendMessage");
}
const user = await db
.table("users")
.filter(q => q.eq(q.field("tokenIdentifier"), identity.tokenIdentifier))
.unique();
const message = {
channel,
format,
body,
user: user._id,
};
db.insert("messages", message);
}
);

We'll also need to update the MessageView component to read the message type and include the GIPHY iframe embed:

src/App.tsx
function MessageView(props: { message: MessageWithAuthor }) {
const message = props.message;
if (message.format == "giphy") {
return (
<div>
<div>
<strong>{message.author}:</strong>
</div>
<iframe src={message.body} />
<div className="giphy-attribution">Powered By GIPHY</div>
</div>
);
}
return (
<div>
<strong>{message.author}:</strong> {message.body}
</div>
);
}

Finally we need to update handleSendMessage to detect /giphy strings and send a POST request to our external function:

src/App.tsx
  const [newMessageText, setNewMessageText] = useState("");
async function handleSendMessage(event: FormEvent) {
event.preventDefault();
setNewMessageText(""); // reset text entry box

// If a /giphy command is entered call into the Netlify function to post
// relevant GIF to channel.
if (newMessageText.startsWith("/giphy ")) {
await fetch("/.netlify/functions/post-gif", {
method: "POST",
body: JSON.stringify({
channel: props.channelId.id,
token: props.idToken,
query: newMessageText.slice(7),
}),
});
} else {
await sendMessage(props.channelId, "text", newMessageText);
}
}
Note that we pass the `idToken` in the POST request -- we obtained this earlier from Auth0 in our `useEffect` hook, and can add a new `useState` hook to store it and update the value when it changes:
src/App.tsx
const [idToken, setIdToken] = useState<string | null>(null);
// Pass the ID token to the Convex client when logged in, and clear it when logged out.
// After setting the ID token, call the `storeUser` mutation function to store
// the current user in the `users` table and return the `Id` value.
useEffect(() => {
if (isLoading) {
return;
}
if (isAuthenticated) {
getIdTokenClaims().then(async claims => {
// Get the raw ID token from the claims.
const token = claims!.__raw;
setIdToken(token);
// Pass it to the Convex client.
convex.setAuth(token);
// Store the user in the database.
const id = await storeUser();
setUserId(id);
});
} else {
// Tell the Convex client to clear all authentication state.
convex.clearAuth();
setUserId(null);
}
}, [isAuthenticated, isLoading, getIdTokenClaims, convex, storeUser]);

We also need to thread it as a prop into ChatBox:

function ChatBox(props: { channelId: Id<"channels">; idToken: string | null }) {
channelId ? <ChatBox channelId={channelId} idToken={idToken} /> : null

That's all the code changes done! Before we can test anything you need to push the new version of sendMessage to Convex:

npx convex push

Now fire up netlify dev or vercel dev again and try posting a GIF to the channel:

GIF in chat

Deployment

If you followed the previous tutorial on hosting and deployment then deployment is almost simple as committing your changes to your repository and running a git push! Netlify and Vercel automatically watch your repository for changes and publish these to your production site.

tip

Don't forget to add convex.json to your source code repository, otherwise the hosted function won't know what Convex deployment to connect to.

Now that you can interface Convex with a general-purpose execution environment you all set to go build the next great web app. If you come up with something cool we'd love to know about it!