DEV Community

linou518
linou518

Posted on

How We Let Users Register with Just a Phone Number on a NOT NULL Email Column

How We Let Users Register with Just a Phone Number on a NOT NULL Email Column

When we bolted a shop frontend onto an existing ERP system, we hit a quiet but annoying authentication problem.

The user table's email column was NOT NULL + UNIQUE. The ERP managed accounts by email. But the shop's target users were wholesale buyers—people who live on their phones and LINE. Many don't have an email address, or at least don't want to type one into a registration form.

The solution: placeholder emails.

System Architecture

ERP Backend (Node.js + TypeScript)
  └─ User model: email (unique, not null), phone, name, role
  └─ /api/shop/auth/register  ← called by Shop
  └─ /api/shop/auth/login

Shop Frontend (React + TypeScript)
  └─ Register.tsx: phone + name + password (email optional)
  └─ Login.tsx: phone + password
Enter fullscreen mode Exit fullscreen mode

The shop and ERP share the same User table. ERP users have role: 'staff' or 'admin'; shop customers get role: 'customer'.

Backend: Sneaking Past the Constraint with Placeholder Emails

The core of the shopRegister endpoint:

// If email is empty or missing, generate a placeholder from the phone number
const email = req.body.email || `shop_${phone}@shop.local`;

// Check phone uniqueness
const existingUser = await User.findOne({ where: { phone } });
if (existingUser) {
  res.status(400).json({ error: 'Phone number already registered' });
  return;
}

// Also check placeholder email uniqueness (belt and suspenders)
const existingEmail = await User.findOne({ where: { email } });
if (existingEmail) {
  res.status(400).json({ error: 'Phone number already registered' });
  return;
}

const user = await User.create({ email, phone, name, password, role: 'customer' });
Enter fullscreen mode Exit fullscreen mode

The format is shop_08012345678@shop.local. The .local domain doesn't actually exist, but it satisfies the DB's unique constraint. If the frontend sends a real email, that takes priority.

Frontend: Email Marked as Optional

// Register.tsx (excerpt)
const handleSubmit = async (e: React.FormEvent) => {
  await register(phone, password, name, email || undefined);
};

// Email field label
<label>
  Email <span className="text-gray-400 text-xs">(optional)</span>
</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
  placeholder="example@email.com" /* no required attribute */
/>
Enter fullscreen mode Exit fullscreen mode

Just remove required. When email is empty, the backend substitutes the placeholder. Passing email || undefined on the frontend keeps empty strings out of the API payload.

Login: Just Search by Phone

export const shopLogin = async (req: Request, res: Response): Promise<void> => {
  const { phone, password } = req.body;
  const user = await User.findOne({ where: { phone } });
  if (!user || !(await user.validatePassword(password))) {
    res.status(401).json({ error: 'Invalid phone number or password' });
    return;
  }
  // ... issue JWT
};
Enter fullscreen mode Exit fullscreen mode

Login searches by phone. Simple. Email isn't involved.

What We Actually Tried First

Initially, we planned a DB migration to make email nullable. But the ERP codebase assumed email was always present in too many places. The migration cost was high.

Next idea: separate table for shop users. But then we'd need two JWT auth middleware stacks.

The placeholder approach feels "hacky"—but zero impact on existing code is a powerful argument. The ERP never sends emails to @shop.local addresses, and the shop UI never displays the email field. No real harm so far.

Takeaways

  • When you need to absorb new frontend requirements without breaking existing DB constraints, placeholder data is a valid escape hatch
  • But make sure future developers understand "this field may not contain meaningful data"—through comments, field naming (email_or_placeholder), or documentation
  • Without that clarity, it becomes tech debt

This was meant to be temporary, but whether we'll actually migrate to a nullable email column remains TBD. "If it works, don't touch it" energy is strong.

Top comments (0)