DEV Community

linou518
linou518

Posted on

Binding LINE Bot Users to Your ERP with Phone Number Matching

The Problem: Two Separate ID Spaces

When you build a chatbot that connects LINE to an ERP system, the first obstacle you hit is identity: "Who exactly is this LINE user in our ERP?"

LINE user IDs look like U2eec19b8c3961a95f436ca8eb185e7f0 — completely separate from your ERP's internal integer user IDs. Before a bot can perform any ERP operations on behalf of a user, you need a way to link these two identities.

Common approaches:

  1. Login flow — Send the user a link to your ERP auth page, let them sign in via LINE Login. Rich UX, but high implementation cost.
  2. Invite code — Admin generates a one-time code, user types it into the bot. Secure but friction-heavy.
  3. Phone number matching — Match against the phone number already registered in ERP. Simple, perfect for internal staff use.

I went with phone number matching since this was for internal employees only.

DB Design: Just Two Columns

ALTER TABLE users 
  ADD COLUMN line_user_id VARCHAR(64) UNIQUE NULL,
  ADD COLUMN line_display_name VARCHAR(255) NULL;
Enter fullscreen mode Exit fullscreen mode

Add two columns to the existing users table. Sequelize's sync() handles migration automatically.

// User.ts (Sequelize Model)
@Column({ type: DataType.STRING(64), unique: true, allowNull: true })
line_user_id!: string | null;

@Column({ type: DataType.STRING(255), allowNull: true })
line_display_name!: string | null;
Enter fullscreen mode Exit fullscreen mode

API Design: Three Endpoints

GET  /api/line/user/:line_user_id   # Look up identity by LINE ID
POST /api/line/bind                  # Bind via phone number
POST /api/line/unbind                # Remove binding
Enter fullscreen mode Exit fullscreen mode

Bind Endpoint

// lineController.ts
export const bindUser = async (req: Request, res: Response) => {
  const { line_user_id, line_display_name, phone } = req.body;

  // Find ERP user by phone number
  const user = await User.findOne({ where: { phone } });
  if (!user) {
    return res.status(404).json({ error: 'Phone number not registered in ERP' });
  }

  // Bind LINE ID
  await user.update({ line_user_id, line_display_name });

  return res.json({
    success: true,
    role: user.role,
    permissions: getPermissions(user.role)
  });
};
Enter fullscreen mode Exit fullscreen mode

Identity Lookup Endpoint

export const getLineUser = async (req: Request, res: Response) => {
  const { line_user_id } = req.params;

  const user = await User.findOne({ where: { line_user_id } });
  if (!user) {
    return res.json({ bound: false });
  }

  return res.json({
    bound: true,
    role: user.role,
    name: user.name,
    permissions: getPermissions(user.role)
  });
};
Enter fullscreen mode Exit fullscreen mode

Permission Groups

ERP roles are simplified to admin and staff (user). Customers don't interact with the bot.

const getPermissions = (role: string): string[] => {
  if (role === 'admin') {
    return ['sales', 'purchases', 'inventory', 'products', 
            'customers', 'suppliers', 'payments', 'reports', 'staff'];
  }
  if (role === 'user') {
    return ['inventory', 'products', 'purchases', 'sales', 'shipments'];
  }
  return []; // Unbound or unknown role
};
Enter fullscreen mode Exit fullscreen mode

Simple, but it cleanly separates "can view inventory" from "can access financial reports."

Bot Message Flow

Receive LINE message
    ↓
GET /api/line/user/:line_user_id — identity check
    ↓
bound: false → "Please send your phone number"
               POST /api/line/bind — execute binding
    ↓
bound: true  → Call ERP API based on permissions
              → Reply via LINE
Enter fullscreen mode Exit fullscreen mode

Checking bind status on every message means no session management is needed. The bot stays stateless — restarts don't require users to re-bind.

Lessons Learned

Phone matching pitfall: If a staff member's phone number isn't registered in the ERP yet, they can never bind. You need to ensure all staff are pre-registered in ERP before onboarding them to the bot. Obvious in hindsight, easy to miss in practice.

TypeScript null guards: User.findOne() returns User | null. Sequelize model types can be confusing here — always add a null guard before performing operations, or TypeScript will fight you.

Stateless bot design pays off: Not having to manage session files is a real quality-of-life win. The bot is simpler, deployments are cleaner, and users don't lose context when the process restarts.

Summary

For internal employee bots, phone number matching + LINE ID binding is the simplest viable authentication design:

  • Just 2 extra DB columns
  • 3 API endpoints (lookup / bind / unbind)
  • Bot stays fully stateless

If authentication gets complex, people stop using the bot. Keep it simple.

Top comments (0)