PR 8 of the multi-user accounts series. Flips the WhatsApp webhook,
automations engine, and flows engine from per-user tenancy to
per-account so a shared inbox actually behaves like one.
Why this matters
- Pre-this-PR: an inbound message resolved the recipient by
whatsapp_config.user_id and dispatched only that user's
automations / flows. Teammates joining via an invite would see
the message in the shared inbox but their automations / flows
never fired.
- Also fixes incipient NOT NULL violations from migration 017.
That migration added `account_id NOT NULL` to automation_logs,
automation_pending_executions, flow_runs, and deals. Inserts in
the engines hadn't been updated yet — any production run would
500 with a column violation. This PR adds account_id to every
such insert.
Surface
- `src/app/api/whatsapp/webhook/route.ts`
- Resolves `config.account_id` AND `config.user_id` from the
matched whatsapp_config row. Passes accountId through every
downstream call. user_id is kept as `configOwnerUserId` and
used only for NOT NULL FK audit columns on contact /
conversation inserts.
- `findOrCreateContact` / `findOrCreateConversation` look up by
account_id and insert with both columns.
- `flagBroadcastReplyIfAny` matches broadcasts by account_id so
any teammate's broadcast is updated when a customer replies.
- `dispatchInboundToFlows` and `runAutomationsForTrigger` now
take `accountId` instead of `userId`.
- `src/lib/automations/engine.ts`
- `DispatchInput.userId` → `accountId`.
- `runAutomationsForTrigger` queries automations by account_id.
- `executeAutomation` inserts into automation_logs with both
account_id (tenancy) and user_id (audit, from automation row).
- `wait` step inserts into automation_pending_executions with
account_id.
- `create_deal` step inserts into deals with account_id.
- `assign_conversation` / `close_conversation` /
`resolveConversationId` filter by account_id.
- `resumePendingExecution` row shape now includes account_id;
cron passes it through.
- engineSendText / engineSendTemplate calls pass
`accountId: args.automation.account_id`.
- `src/lib/flows/engine.ts`
- `DispatchInboundInput.userId` is kept (audit) and
`accountId` is added (tenancy).
- `loadActiveRunForContact`, `isDuplicateInbound`,
`findEntryFlow` all query by account_id.
- `startNewRun` inserts into flow_runs with account_id.
- All `engineSend*` calls pass `accountId: run.account_id`.
- `src/lib/automations/meta-send.ts` + `src/lib/flows/meta-send.ts`
- All four engine senders (text, template, media, interactive
buttons/list) accept `accountId` and look up the contact +
whatsapp_config by account_id. user_id stays for inserts.
- `src/lib/flows/types.ts`
- `FlowRow`, `FlowRunRow`, `DispatchInboundInput` gain
`account_id` / `accountId`.
- `src/types/index.ts`
- `Automation` gains `account_id`. Doc note on the user_id
field clarifying it's audit-only post-multi-user.
- `src/app/api/automations/cron/route.ts`
- Pass account_id from the pending row to resumePendingExecution.
- `src/app/api/automations/engine/route.ts`
- Uses `getCurrentAccount()` from #169 to resolve the caller's
account, then dispatches with accountId.
- `CHANGELOG.md` — Fixed entry explaining the inbox / NOT NULL fix.
Backwards compatibility
- All affected tables already have account_id from migration 017.
This PR populates it on insert paths that hadn't been updated.
- The `user_id` columns are kept everywhere — they still identify
the agent / author for audit display. They just no longer
enforce tenancy.
Verification
- `npm run lint` → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean (had 4 errors mid-refactor; all
resolved by the call-site updates)
- `npm test` → 366/366 pass (engine tests are pure unit
tests over helper functions; not affected)
- `npm run build` → succeeds
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Activating an automation with obviously-broken config (add_tag step
with empty tag_id, send_message with empty text, keyword_match
trigger with no keywords, etc.) used to succeed — and then every
dispatch produced a cryptic failed log row that users often missed.
- New validate.ts centralises the checks (mirrors engine.ts's
runtime guards one layer earlier so failures surface at save time).
Returns { path, message } issues so the UI can point users at the
exact problem.
- POST /api/automations rejects with 400 when is_active === true and
the merged config fails validation.
- PATCH /api/automations/[id] computes the post-patch "effective"
state (merging body fields over the existing row, loading current
steps if body.steps isn't provided) and applies the same gate.
Drafts (is_active=false) remain free to be incomplete so users can
still save progress mid-build.
- Builder surfaces the first issue in a toast with path as the
description, so users don't need DevTools to find what to fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the /automations placeholder with a full feature:
- Migration 006 adds automations, automation_steps, automation_logs,
and automation_pending_executions with RLS matching existing tables.
- List page with active/inactive switch, quick-start templates,
duplicate, delete, and View Logs actions.
- Full-screen vertical flow builder with Trigger + Action + Condition
(Yes/No branches) + Wait cards. Inline edit forms, up/down reorder,
dotted-grid canvas.
- Execution engine (src/lib/automations/engine.ts) dispatched from the
WhatsApp webhook for new_message_received, new_contact_created, and
keyword_match. Wait steps enqueue to automation_pending_executions;
/api/automations/cron drains them behind AUTOMATION_CRON_SECRET.
- Logs viewer with per-step checkmarks and expandable detail.
Existing files touched only in webhook/route.ts (fire-and-forget
trigger dispatch) and types/index.ts (appended automation types).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>