Read this first when resuming. It records a multi-task session that simplified ForgeCRM OSS, module by module, into a single-owner, stripped-down WhatsApp CRM. Date: 2026-05-22. Login:
admin@forgemind.space/ yourADMIN_PASSWORD(or the password printed in the backend logs on first boot). App: backend:3001, frontend:5173.
Transformed the CRM from a multi-user, multi-WABA, feature-rich product into a lean single-owner, single-WhatsApp-account tool. Each task followed the user's pattern: Keep X / Remove Y / Requirements. Work was non-destructive to data (DB rows/tables/columns preserved; only UI + routes + logic removed). Every task ended with a build + backend-boot + functional verification.
- Non-destructive: never drop tables/columns or delete records. Remove UI, routes, and logic only. Data for removed features is left in place (preserved/viewable).
- Layered enforcement when "limiting" something: frontend UX restriction + backend hard guard + runtime tolerance + (if needed) one-time migration.
- Verify every task:
cd frontend && npm run build; restart backend (node src/index.js) and hit/health+ a functional API check via PowerShellInvoke-WebRequest. - Dead-code detector (recreate as
frontend/find-dead.cjs, run, then delete it): finds unused imports + cross-file-unreferenced top-level declarations. NOTE the regex must count...spreadandobj.memberas usage (lookbehind(?<![\w$]), NOT(?<![\w$.])) or it false-positives style spreads. - Vite build does NOT catch undefined identifiers (no symbol resolution) — only unresolved imports. So removing a used component fails at runtime, not build. Verify declaration removals by reference-count, not just by building.
- Parked code = code for a disabled feature that's still reachable-in-principle (e.g. automation node-type panels, engine handlers). The user chose to LEAVE parked code in place ("Safe: only provably-unreferenced"). Do not rip it out unless asked.
-
Home / Dashboard — single-user scope; removed team leaderboard. (Conversation-volume trend chart removed from UI earlier; its backend query was later removed in the Categories task.) Files:
backend/src/routes/dashboard.js,frontend/src/pages/HomePage.jsx. -
Chats — polling refresh only. Removed real-time push (SSE), reactions, quote-reply, starred. Deleted
backend/src/events.js+backend/src/routes/events.js+frontend/src/hooks/useServerEvents.js; cleanedmessages.js,sendQueue.js,metaSend.js,index.js, ChatWindow/MessageBubble/ContactList/NumberSidebar. -
Contacts — kept list / view / save / apply tags. Removed custom fields, contact assignment, ownership, team ownership, and roles ("everyone sees all").
permissions.jsstubbed to allow-all;contactFields.jsmade read-only (GET only); Admin Settings Fields tab removed. -
Message Templates — kept create / submit-to-Meta / approval status / live phone preview. Removed analytics, revision history, template library, version tracking. Preserved Meta API integration.
-
Media — kept upload / preview / use. Per-account ownership (media belongs to the connected account). Removed shared libraries + multi-account access. Migration 039_media_account_owner.sql.
-
Bulk Messaging — kept broadcast / template send / plain-text send / per-recipient logs. Removed variable mapping, dynamic placeholders, scheduling, media-header campaigns, advanced builder. Migration 040_broadcast_logs_fields.sql (added
wa_message_id,error_message; widened status CHECK). -
Automations — Keyword Trigger + Send Message only, linear flows. Removed branching, conditions, delays, multi-step, all other node types. Enforcement is layered:
backend/src/routes/chatbots.js→sanitizeToLinear(config)collapses any saved config to trigger→message→message on POST/PUT (exported for reuse).backend/src/engine/automationEngine.js→NODE_HANDLERS={ trigger, message };walkFromfollows thedefaultedge linearly and logsskippedfor unknown node types;evaluateTriggersfires keyword triggers only;resumeAutomationresumes at the default-edge child.backend/scripts/migrateLinearAutomations.js(one-time; ran 0/0 locally).frontend/src/components/AutomationBuilderView.jsx→ palette reduced to 2 blocks; trigger dropdown locked to keyword.- Dead engine handlers (
executeConditionNode/Delay/Action/Handoff/AI/API/Subflow) left in place as parked code per user choice.
-
Admin Tags — kept Create/Edit/Delete (tags require a category, so categories stay). No functional change. Did an app-wide dead-code sweep: removed unused imports across ~11 files, dead declarations (
fmtN,Chip,PreviewBubble,PreviewQR,DIRECT_MSG_MAP,isImageMime,TIME_RANGES,PlaceholderTab,formatDateForInput), the orphanedConfirmDialog.jsx, and the backendALERTABLE_TYPESconst. -
Admin Categories — kept Create/Edit/Delete, no functional change. Removed the one remaining unused removed-module reference: the
trenddaily-aggregation query + response key indashboard.js. -
WhatsApp Accounts — exactly one account. Backend
whatsappAccounts.js: POST returns 409 if one already exists; lone account forcedis_default=true; DELETE refuses to remove the last account (switch numbers by editing). Admin tab kept as an editable single-account view (no Add when ≥1, no Delete, no default/switch UI — but token/details still editable). Per-feature account pickers hidden, auto-use the one account (Templates, WA Links, Automations send-from + listen-on filter). MediaLibrary already guarded; Broadcasts has no picker (resolves server-side). -
Authentication & access — single owner login + JWT kept; removed roles, permissions, teams, multi-user, invitations.
backend/src/auth.js: session ={id,username,email,displayName,isActive}, JWT payload ={id,username,displayName}. Deletedroutes/users.js(multi-user + invitations + audit-log) androutes/teamMembers.js(teams); unmounted both inindex.js. Frontend: removed Users + Team Members admin tabs (excised ~760 lines incl.RoleBadge/ROLE_OPTIONS/UsersTab/TeamMembersTab), dropped App.jsx page guard + Sidebar nav filter (owner sees all), removed role refs in Topbar/HomePage, trimmedapi.js(users/teamMembers/auditLog), converted the broadcast "test recipient" team-member pickers (Contacts + Broadcasts) to plain number inputs. Visibility-scoping inmessages.js/dashboard.js/middleware/access.js/permissions.jsleft inert (owner = admin viaisAdmin()→true) per user choice.
# Infra (Docker Desktop must be running)
docker compose -p forgecrm-local -f "local-infra/docker-compose.yml" up -d # pg :5432, redis :6379, minio :9000
# Backend
cd "ForgeCRM OSS/backend"; node src/index.js # http://localhost:3001 (/health -> {"ok":true})
# Frontend
cd "ForgeCRM OSS/frontend"; npm run dev # http://localhost:5173- Login
admin@forgemind.space/ADMIN_PASSWORD(owner account, id=1 — preserved). - Backend was running as a background process during the session; it stops when the shell ends — restart with the command above.
- Stale project memory:
ForgeCRM OSS/CLAUDE.md,backend/CLAUDE.md,frontend/CLAUDE.mdstill describe removed features (SSE/events.js/useServerEvents, Fields tab, Users/Team tabs, roles/RBAC,templateAnalytics.js, multi-account, automation node types). Offered to refresh; user hasn't asked yet. - Non-destructive DB leftovers (data kept, logic gone): columns
forgecrm_users.role,forgecrm_users.permissions,contacts.assigned_user_id; tablesuser_wa_assignments,user_audit_log,broadcast_variable_mapping,message_reactions,contact_field_definitions,team_members. No migration written to drop them (would be destructive) — do only if asked. - Parked code retained: automation engine handlers + builder node-type panels (handoff/action/etc.), the dashboard/messages visibility-scoping (inert), the no-op "Delete account" placeholder in General settings.
- No real Meta WABA configured locally → live WhatsApp send/receive is inert until an account is added in Admin Settings → WhatsApp Accounts (now capped at one).
A single-owner CRM: one login, one WhatsApp account, linear keyword→message automations, template + text broadcasts with per-recipient logs, contacts with tags, media scoped to the account, and a single-user dashboard. Frontend bundle shrank from ~860 kB to ~826 kB over the cleanup passes.