phone + password (no OTP)?

hey guys, i was in the middle of migrating from supabase auth to better-auth when i hit a snag. basically i want to allow a single admin user of an orgazniation to create users with phone and password. then said users can sign in with phone and password, without OTP . I am thinking we will only use OTP for forgot password flows, but since this is a business app, probably just put the onus on the admin to reset forgotten passwords in app. does anyone know if this is possible with better-auth? i saw a hacky way to do this by using the better-auth user.email field but doesnt feel right any help would be appreciated!
5 Replies
Ping
Ping3mo ago
Yeah you will need to use some work-around since we don't have a native solution to this. Alternatively you can build your own phone number auth plugin
paisano_luciano
paisano_lucianoOP3mo ago
Im fairly new to engineering in general @Ping my apologies if this sounds ignorant, but would a new plugin be secure? Does this add complexity?
Ping
Ping3mo ago
Everything in Better-auth works around plugins, the phone-otp itself is a plugin, for example. It will add complexity, but so does coding anything in your app
paisano_luciano
paisano_lucianoOP3mo ago
got it @Ping so something like this?
// plugins/admin-create-phone-user/index.ts
import { serverPlugin } from "better-auth/server";
import { createPhoneUserInput } from "./schema";

export const adminCreatePhoneUser = serverPlugin({
name: "adminCreatePhoneUser",

register(app, ctx) {
app.post("/admin/create-phone-user", async (req, res) => {
const session = await ctx.auth.session.get(req);
if (!session?.user?.isAdmin) {
return res.status(403).json({ error: "Forbidden" });
}

const result = createPhoneUserInput.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: "Invalid input", details: result.error });
}

const { phoneNumber, password, name, teammemberId } = result.data;

try {
const user = await ctx.auth.user.create({
data: {
name,
phoneNumber,
phoneNumberVerified: true, // <-- ✅ skips OTP requirement
teammemberId, // <-- link to local teammember table
},
credentials: {
providerId: "credentials",
accountId: phoneNumber,
password,
},
});

return res.status(201).json({ userId: user.id });
} catch (err) {
console.error("User creation error:", err);
return res.status(500).json({ error: "Internal server error" });
}
});
},
});
// plugins/admin-create-phone-user/index.ts
import { serverPlugin } from "better-auth/server";
import { createPhoneUserInput } from "./schema";

export const adminCreatePhoneUser = serverPlugin({
name: "adminCreatePhoneUser",

register(app, ctx) {
app.post("/admin/create-phone-user", async (req, res) => {
const session = await ctx.auth.session.get(req);
if (!session?.user?.isAdmin) {
return res.status(403).json({ error: "Forbidden" });
}

const result = createPhoneUserInput.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: "Invalid input", details: result.error });
}

const { phoneNumber, password, name, teammemberId } = result.data;

try {
const user = await ctx.auth.user.create({
data: {
name,
phoneNumber,
phoneNumberVerified: true, // <-- ✅ skips OTP requirement
teammemberId, // <-- link to local teammember table
},
credentials: {
providerId: "credentials",
accountId: phoneNumber,
password,
},
});

return res.status(201).json({ userId: user.id });
} catch (err) {
console.error("User creation error:", err);
return res.status(500).json({ error: "Internal server error" });
}
});
},
});
side question: does better-auth have an llms.txt file for context to models during dev?

Did you find this page helpful?