Plugins

OTP

Official plugin — one-time codes over SMS, WhatsApp, or email under /auth, with explicit provider or app mode.

otp() signs people in with a one-time code. You must set mode — there is no auto-detect. Provider mode is SMS Verify via the bound driver; app mode is app-owned delivery across the channels you declare.

The one rule

Enable gate.auth, then .plug(otp({ mode: "provider" })) or .plug(otp({ mode: "app", channels: [...] })). Never omit mode. Never log raw OTPs.

One otp() per app

Provider and app mode cannot both be active — they claim the same fixed /auth/otp/* routes. Need two OTP-like mechanisms? Combine otp() with a different plugin (e.g. magicLink()), not a second otp().

fx.sendOtp vs otp()

fx.sendOtp / fx.verifyOtp are raw Channel capabilities — no .plug(). Direct use means you build routes, sessions, rates, and storage yourself. otp() provider mode wraps that path; skip it and call the raw methods in your own flow without losing the provider connection.

Quick start

Plug app mode (multi-channel)

src/app.ts
import { oke } from "okengine";
import { otp } from "okengine/plugins";

export const app = oke({
  name: "shop",
  env: "dev",
  gate: { auth: {} },
}).plug(
  otp({
    mode: "app",
    channels: ["sms", "whatsapp", "email"],
    exposeDevOtp: true, // local DX only
  }),
);

Request a code

const { data } = await api.auth.requestOtp({
  phone: "+15551234567",
  email: "ali@example.com",
});

POST /auth/otp/request. Prior active challenges for that principal are invalidated. Delivery follows channels order for addresses you pass.

Resend on another channel (app mode only)

const { data } = await api.auth.resendOtp({
  phone: "+15551234567",
  email: "ali@example.com",
  channel: "email",
});

Same code, same TTL. Default cooldown is 60 seconds. Provider mode has no resend surface — the provider owns the code.

Verify

const { data } = await api.auth.verifyOtp({
  phone: "+15551234567",
  otp,
});

POST /auth/otp/verify — five failed attempts consume the challenge.

Pre-account hijack defense (email)

App-mode email verify proves inbox ownership. An unverified parked email+password account is reclaimed (sessions revoked, hashes cleared, emailVerified set) before the OTP session. Verified owners re-auth only.

Modes

Provider modeApp mode
Configotp({ mode: "provider" })otp({ mode: "app", channels: [...] })
Who owns the codeProvider (Verify API)Your app
Deliveryfx.sendOtp / fx.verifyOtpfx.deliverOtp (Channel templates)
ChannelsSMS onlysms · whatsapp · email (declared order)
Resend other channelImpossiblePOST /auth/otp/resend
exposeDevOtpForbiddenOptional (default off)

Provider mode limitation

Resend-via-different-channel is impossible in provider mode — the code value is never visible to OKE. Use app mode when you need SMS → email fallback for the same code.

Provider mode setup

oke.config.ts
export default {
  drivers: {
    channel: {
      sms: { test: "console", prod: "taqnyat" },
    },
  },
};

Boot fails loudly if no SMS driver exposes sendOtp / verifyOtp. Switch to app mode, or bind a Verify-capable driver (for example taqnyat).

App mode delivery

ConcernBehavior
StorageSHA-256 hash for verify + sealed AES-GCM copy (HKDF oke-otp-seal-v1) for redelivery
Seal lifetimeWiped on verify, lockout, or TTL expiry — never left after the challenge dies
Challenge TTLDefault 10 minutes (ttlMs)
Resend cooldownDefault 60 seconds (resendCooldownMs) — separate from TTL
Auto failoverOn real provider send errors, sently FallbackTransport walks remaining media; Taqnyat WhatsApp may use sendWithFailover
User resendExplicit resend with channel — not automatic

Templates: auth-otp-email, auth-otp-sms, auth-otp-whatsapp (EN/AR, {{otp}}). SMS here is a plain message — not Taqnyat Verify.

Options

OptionTypeDefaultMeaning
mode"provider" | "app"requiredDelivery mechanism — no auto
channels("sms"|"whatsapp"|"email")[]required in app modeBuild-time preferred order
ttlMsnumber10mChallenge lifetime
resendCooldownMsnumber60sApp-mode resend spacing
exposeDevOtpbooleanfalseApp mode only — raw OTP in response
fromstringOKE <no-reply@oke.local>Email template From
secretstringactive*Auth secret (*from gate.auth)
sessionsSessionStoreactive*Session store
identitiesIdentityStorenewEmail → user
phonesPhoneStorenewPhone → user
verificationsVerificationStorenewChallenge store

Surfaces

FlowPathGateMode
auth.requestOtpPOST /auth/otp/requestgate.public + otp rateboth
auth.verifyOtpPOST /auth/otp/verifygate.public + otp rateboth
auth.resendOtpPOST /auth/otp/resendgate.public + otp rateapp mode only

Troubleshooting

Learn more

  • Magic link — link instead of a code
  • Two-factor — email OTP / TOTP as a second factor after password (distinct from this primary /auth/otp sign-in)
  • Channelfx.send, fx.sendOtp, drivers, Mailpit
  • Gategate.auth

Next

On this page