OTP API | Next.js Supabase SaaS Kit
The OTP API is a simple interface for working with one-time passwords (OTPs) in your application.
This package provides a complete solution for generating, sending, and verifying one-time passwords or tokens in your application. It integrates with Supabase for secure token storage and verification.
It is used for various destructive actions in the SaaS Kit, such as deleting accounts, deleting teams, and deleting users. However, you can use it for a variety of other purposes as well, such as:
- Your custom destructive actions
- oAuth account connections
- etc.
Overview
The OTP package offers:
- Secure Token Generation: Create time-limited tokens with configurable expiration
- Email Delivery: Send OTP codes via email with customizable templates
- Verification UI: Ready-to-use verification form component
- Token Management: Revoke, verify, and check token status
Installation
If you're using Makerkit, this package is already included. For manual installation:
pnpm add @kit/otp
Basic Usage
Creating and Sending an OTP
To create and send an OTP, you can use the createToken
method:
import { createOtpApi } from '@kit/otp/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';// Create the API instanceconst client = getSupabaseServerClient();const api = createOtpApi(client);// Generate and send an OTP emailawait api.createToken({ userId: user.id, purpose: 'email-verification', expiresInSeconds: 3600, // 1 hour metadata: { redirectTo: '/verify-email' }});// Send the email with the OTPawait api.sendOtpEmail({ email: userEmail, otp: token.token});
Verifying an OTP
To verify an OTP, you can use the verifyToken
method:
// Verify the tokenconst result = await api.verifyToken({ token: submittedToken, purpose: 'email-verification'});if (result.valid) { // Token is valid, proceed with the operation const { userId, metadata } = result; // Handle successful verification} else { // Token is invalid or expired // Handle verification failure}
Server Actions
The package includes a ready-to-use server action for sending OTP emails:
import { sendOtpEmailAction } from '@kit/otp/server/server-actions';// In a form submission handlerconst result = await sendOtpEmailAction({ email: userEmail, purpose: 'password-reset', expiresInSeconds: 1800 // 30 minutes});if (result.success) { // OTP was sent successfully} else { // Handle error}
NB: the email
parameter is only used as verification mechanism, the actual email address being used is the one associated with the user.
Verification UI Component
The package includes a ready-to-use OTP verification form:
import { VerifyOtpForm } from '@kit/otp/components';function MyVerificationPage() { return ( <VerifyOtpForm purpose="password-reset" email={userEmail} onSuccess={(otp) => { // Handle successful verification // Use the OTP for verification on the server }} CancelButton={ <Button variant="outline" onClick={handleCancel}> Cancel </Button> } /> );}
API Reference
createOtpApi(client)
Creates an instance of the OTP API.
Parameters:
client
: A Supabase client instance- Returns: OTP API instance with the following methods:
api.createToken(params)
Creates a new one-time token.
Parameters:
params.userId
(optional): User ID to associate with the tokenparams.purpose
: Purpose of the token (e.g., 'password-reset')params.expiresInSeconds
(optional): Token expiration time in seconds (default: 3600)params.metadata
(optional): Additional data to store with the tokenparams.description
(optional): Description of the tokenparams.tags
(optional): Array of string tagsparams.scopes
(optional): Array of permission scopesparams.revokePrevious
(optional): Whether to revoke previous tokens with the same purpose (default: true)
Returns:
{ id: string; // Database ID of the token token: string; // The actual token to send to the user expiresAt: string; // Expiration timestamp revokedPreviousCount: number; // Number of previously revoked tokens}
api.verifyToken(params)
Verifies a one-time token.
Parameters:
params.token
: The token to verifyparams.purpose
: Purpose of the token (must match the purpose used when creating)params.userId
(optional): User ID for additional verificationparams.requiredScopes
(optional): Array of required permission scopesparams.maxVerificationAttempts
(optional): Maximum allowed verification attempts
Returns:
{ valid: boolean; // Whether the token is valid userId?: string; // User ID associated with the token (if valid) metadata?: object; // Metadata associated with the token (if valid) message?: string; // Error message (if invalid) scopes?: string[]; // Permission scopes (if valid) purpose?: string; // Token purpose (if valid)}
api.revokeToken(params)
Revokes a token to prevent its future use.
Parameters:
params.id
: ID of the token to revokeparams.reason
(optional): Reason for revocation
Returns:
{ success: boolean; // Whether the token was successfully revoked}
api.getTokenStatus(params)
Gets the status of a token.
Parameters:
params.id
: ID of the token
Returns:
{ exists: boolean; // Whether the token exists purpose?: string; // Token purpose userId?: string; // User ID associated with the token createdAt?: string; // Creation timestamp expiresAt?: string; // Expiration timestamp usedAt?: string; // When the token was used (if used) revoked?: boolean; // Whether the token is revoked revokedReason?: string; // Reason for revocation (if revoked) verificationAttempts?: number; // Number of verification attempts lastVerificationAt?: string; // Last verification attempt timestamp lastVerificationIp?: string; // IP address of last verification attempt isValid?: boolean; // Whether the token is still valid}
api.sendOtpEmail(params)
Sends an email containing the OTP code.
Parameters:
params.email
: Email address to send toparams.otp
: OTP code to include in the email
Returns: Promise that resolves when the email is sent
Database Schema
The package uses a nonces
table in your Supabase database with the following structure:
id
: UUID primary keyclient_token
: Hashed token sent to clientnonce
: Securely stored token hashuser_id
: Optional reference to auth.userspurpose
: Purpose identifier (e.g., 'password-reset')- Status fields:
expires_at
,created_at
,used_at
, etc. - Audit fields:
verification_attempts
,last_verification_at
, etc. - Extensibility fields:
metadata
,scopes
Best Practices
- Use Specific Purposes: Always use descriptive, specific purpose identifiers for your tokens.
- Short Expiration Times: Set token expiration times to the minimum necessary for your use case.
- Handle Verification Failures: Provide clear error messages when verification fails.
- Secure Your Tokens: Never log or expose tokens in client-side code or URLs.
Example Use Cases
- Email verification
- Two-factor authentication
- Account deletion confirmation
- Important action verification
Each use case should use a distinct purpose identifier. The purpose will always need to match the one used when creating the token.
When you need to assign a specific data to a token, you can modify the purpose with a unique identifier, such as email-verification-12345
.