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:
- 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.
- Invite code — Admin generates a one-time code, user types it into the bot. Secure but friction-heavy.
- 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;
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;
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
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)
});
};
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)
});
};
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
};
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
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)