sbkl
sbkl3mo ago

convex auth OTP email customisation

Below is the code from the doc to send an OTP email to users with convex auth. I have a multi tenant app and I wonder if I can customise the from part. Ideally, retrieve the domain origin from where the request came from and then query the database to retrieve the right tenant and finally customise the from part?
import { Email } from "@convex-dev/auth/providers/Email";
import { Resend as ResendAPI } from "resend";
import { alphabet, generateRandomString } from "oslo/crypto";

export const ResendOTP = Email({
id: "resend-otp",
apiKey: process.env.AUTH_RESEND_KEY,
maxAge: 60 * 15, // 15 minutes
// This function can be asynchronous
generateVerificationToken() {
return generateRandomString(8, alphabet("0-9"));
},
async sendVerificationRequest({ identifier: email, provider, token }) {
const resend = new ResendAPI(provider.apiKey);
const { error } = await resend.emails.send({
from: "My App <onboarding@resend.dev>",
to: [email],
subject: `Sign in to My App`,
text: "Your code is " + token,
});

if (error) {
throw new Error(JSON.stringify(error));
}
},
});
import { Email } from "@convex-dev/auth/providers/Email";
import { Resend as ResendAPI } from "resend";
import { alphabet, generateRandomString } from "oslo/crypto";

export const ResendOTP = Email({
id: "resend-otp",
apiKey: process.env.AUTH_RESEND_KEY,
maxAge: 60 * 15, // 15 minutes
// This function can be asynchronous
generateVerificationToken() {
return generateRandomString(8, alphabet("0-9"));
},
async sendVerificationRequest({ identifier: email, provider, token }) {
const resend = new ResendAPI(provider.apiKey);
const { error } = await resend.emails.send({
from: "My App <onboarding@resend.dev>",
to: [email],
subject: `Sign in to My App`,
text: "Your code is " + token,
});

if (error) {
throw new Error(JSON.stringify(error));
}
},
});
3 Replies
Convex Bot
Convex Bot3mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
erquhart
erquhart2mo ago
sendVerificationRequest has an action ctx as an undocumented second param. Typescript will yell at you so you'll have to have it expect an error there. You can check it out here: https://github.com/get-convex/convex-auth/blob/e4a9e0e7de0da9160185ac0028cfb684a9bcd147/src/server/implementation/signIn.ts#L166-L168 You'll need to assert it's type using ActionCtx from your generated server code. I haven't used it, but looking at the code it should work.
sbkl
sbklOP2mo ago
Oh that's cool! Thanks. Then to identify the tenant with the origin of the request, I have added the redirectTo params on the signIn method, put the SITE_URL setup with convex-auth and added a origin query param that I can retrieve from the url args from the
sendVerificationRequest
sendVerificationRequest
like so Client side:
await signIn("resend-otp", {
email,
redirectTo: `${process.env.NEXT_PUBLIC_PROTOCOL}${process.env.NEXT_PUBLIC_ROOT_DOMAIN}?origin=${window.location.origin}`,
});
await signIn("resend-otp", {
email,
redirectTo: `${process.env.NEXT_PUBLIC_PROTOCOL}${process.env.NEXT_PUBLIC_ROOT_DOMAIN}?origin=${window.location.origin}`,
});
Convex side:
export const ResendOTP = Email({
id: "resend-otp",
apiKey: env.AUTH_RESEND_KEY,
maxAge: 60 * 15, // 15 minutes
async generateVerificationToken() {
const alphabet = "0123456789";
return generateRandomString(random, alphabet, 6);
},
// @ts-expect-error Figure out typing for email providers so they can
// access ctx.
async sendVerificationRequest(
params: SendVerificationRequestParams,
ctx: ActionCtx
) {
const url = new URL(params.url);
const origin = url.searchParams.get("origin");
if (!origin) {
throw new ConvexError({
field: "origin",
message: "Origin couldn't be found",
});
}
const siteName = await ctx.runQuery(internal.sites.internal.query.getSiteNameByDomain, {
origin,
});
const resend = new ResendAPI(params.provider.apiKey);
const { error } = await resend.emails.send({
from: `${siteName} <${env.SENDER_EMAIL}>`,
to: [params.identifier.toLowerCase()],
subject: `Sign in to ${siteName}`,
text: `
Hello,

Your temporary code to access ${siteName} is:

${params.token}

You can use this code to verify your account and access ${siteName}.

This code will expire in 15 minutes.

Best regards,
The ${siteName} Team
`,
});

if (error) {
throw new ConvexError(JSON.stringify(error));
}
},
});
export const ResendOTP = Email({
id: "resend-otp",
apiKey: env.AUTH_RESEND_KEY,
maxAge: 60 * 15, // 15 minutes
async generateVerificationToken() {
const alphabet = "0123456789";
return generateRandomString(random, alphabet, 6);
},
// @ts-expect-error Figure out typing for email providers so they can
// access ctx.
async sendVerificationRequest(
params: SendVerificationRequestParams,
ctx: ActionCtx
) {
const url = new URL(params.url);
const origin = url.searchParams.get("origin");
if (!origin) {
throw new ConvexError({
field: "origin",
message: "Origin couldn't be found",
});
}
const siteName = await ctx.runQuery(internal.sites.internal.query.getSiteNameByDomain, {
origin,
});
const resend = new ResendAPI(params.provider.apiKey);
const { error } = await resend.emails.send({
from: `${siteName} <${env.SENDER_EMAIL}>`,
to: [params.identifier.toLowerCase()],
subject: `Sign in to ${siteName}`,
text: `
Hello,

Your temporary code to access ${siteName} is:

${params.token}

You can use this code to verify your account and access ${siteName}.

This code will expire in 15 minutes.

Best regards,
The ${siteName} Team
`,
});

if (error) {
throw new ConvexError(JSON.stringify(error));
}
},
});

Did you find this page helpful?