wacrm/CHANGELOG.md
Arnas Donauskas 2f0334b0de
feat(db): account-sharing schema foundation
Turns every wacrm install multi-tenant on the database side. A single
user's signup now creates an `accounts` row, links the profile to it
as `owner`, and every domain row is scoped via a new `account_id`
column instead of `user_id`.

Why now: the next handful of PRs add the invite/members user surface
(gated by the `account_sharing` beta flag); that surface needs RLS to
already speak "membership" rather than "ownership". Doing the schema
flip in isolation keeps the diff reviewable and lets the follow-up
PRs stay focused on TS/UI.

What the migration does:
- Introduces `account_role_enum` + `accounts` + `account_invitations`.
- Adds `account_id` to every parent tenant table (16 tables incl.
  profiles) and `account_role` to profiles.
- Backfills one account per existing user, then sets NOT NULL.
- Replaces every `auth.uid() = user_id` RLS policy with a membership
  check via a SECURITY DEFINER `is_account_member(account_id, min)`
  helper. Three tiers: viewers read, agents write operational data,
  admins write settings-class tables (whatsapp_config, templates,
  pipelines, tags, custom_fields).
- Swaps `whatsapp_config.UNIQUE(user_id)` → `UNIQUE(account_id)` and
  `flow_runs` active-run idempotency key to `(account_id, contact_id)`.
- Replaces `handle_new_user` so new signups bootstrap their own
  account atomically.

Backwards compatibility: every existing user is the sole `owner` of
their freshly-created account, every existing row is linked to it,
and the legacy `user_id` columns stay (still used for assignment /
audit, no longer for isolation). Net behavioural change on its own:
zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 12:59:07 +03:00

14 KiB

Changelog

User-visible changes in wacrm. Self-hosters: when pulling an update, check this file for any migration required notes and apply the matching SQL files from supabase/migrations/ against your Supabase project before restarting the app.

Versions follow Keep a Changelog. Pre-1.0, MINOR bumps cover new modules; PATCH bumps cover bug fixes and polish.

[Unreleased]

Foundation for multi-user accounts. Every wacrm install becomes multi-tenant on the database side: a single user's signup creates a fresh "account", and every row is scoped to that account rather than to the user directly. The user-visible invite / members surface lands in follow-up PRs gated by the 'account_sharing' beta feature flag — this release is wiring with no behaviour change on its own. Existing self-hosted instances keep working: every existing user is backfilled as the sole owner of their own account and sees identical data.

Changed

  • Tenancy moves from per-user to per-account. RLS on every domain table (contacts, conversations, messages, broadcasts, automations, flows, pipelines, templates, tags, …) now checks account membership via a new SECURITY DEFINER helper is_account_member(account_id, min_role) instead of auth.uid() = user_id. The user_id columns stay on every row for assignment / audit but no longer enforce isolation.
  • WhatsApp config is one-per-account, not one-per-user. The whatsapp_config.UNIQUE(user_id) constraint is replaced by UNIQUE(account_id).
  • flow_runs idempotency key swaps to (account_id, contact_id) so two accounts sharing a contact phone number can each run their own flows independently.
  • The signup trigger (handle_new_user) now also creates a personal account and links the new profile to it as owner.

Migration required

Apply against your Supabase project before deploying this version:

  • supabase/migrations/017_account_sharing.sql — introduces the accounts and account_invitations tables plus an account_role_enum type; adds account_id to every user-scoped table and backfills it; rewrites every RLS policy; replaces the new-user trigger. Idempotent. No data loss — every existing user is mapped to a freshly-created account with role owner and every existing row of theirs is linked to that account.

[0.2.2] — 2026-05-29

Flow nodes can now send media. Closes the most-requested gap from user feedback after the v0.2.0 Flows launch — flows were text-only and couldn't deliver an invoice, receipt, product photo, or short demo video mid-conversation.

Added

  • send_media flow node. Send an image (PNG / JPEG / WebP), video (MP4 / 3GP), or document (PDF, Word, Excel, PowerPoint, TXT) to the customer from any point in a flow. Pick a file in the builder, it uploads to the new flow-media Supabase Storage bucket, and Meta fetches the public URL at send time. Optional caption (1024 char cap, supports {{vars.X}} interpolation); documents also take an optional filename shown in the recipient's chat. Auto-advances after send — same suspend semantics as send_message. (#156)

Migration required

Apply against your Supabase project before deploying this version:

  • supabase/migrations/016_flow_media.sql — does two things:
    1. Adds 'send_media' to the flow_nodes.node_type CHECK constraint. Without this the send_media node fails to save with a constraint violation.
    2. Creates the public flow-media Supabase Storage bucket (16 MB file-size cap, image / video / document MIME allowlist) plus per-user RLS policies (path prefix = auth.uid()). Without this the builder's file picker fails on upload. Same shape as the avatars bucket from migration 008 — the bucket is public so Meta can fetch the URL without credentials.

The migration is idempotent and safe to re-run.

[0.2.1] — 2026-05-26

Bug-fix release. Plugs a silent inbound-message drop that triggered when two users on the same instance saved the same WhatsApp phone_number_id.

Fixed

  • Inbound WhatsApp messages no longer silently disappear when two users have claimed the same phone_number_id. Previously the webhook used .single() to look up the owning config, which errors PGRST116 for both 0 rows and ≥2 rows — the second user's save put the DB into the ≥2-row state and every inbound message was dropped while the log misleadingly reported "No config found for phone_number_id". Three layers of fix: POST /api/whatsapp/config now returns 409 when another user has already claimed the number, the webhook lookup distinguishes 0 rows from ≥2 rows and logs the conflicting user_ids, and a new DB constraint (UNIQUE(phone_number_id)) prevents the bad state at the storage layer. Reported in #136, fixed in #143.

Migration required

Apply against your Supabase project before deploying this version:

  • supabase/migrations/013_whatsapp_config_phone_number_id_unique.sql — adds UNIQUE(phone_number_id) to whatsapp_config. Fails loudly with a copy-pasteable resolution hint if duplicate rows already exist; auto-deduping would destroy encrypted tokens, so the operator picks which row keeps the number. To check first:

    SELECT phone_number_id, array_agg(user_id) AS owners, count(*) AS n
    FROM whatsapp_config
    GROUP BY phone_number_id
    HAVING count(*) > 1;
    

    If that returns rows, DELETE the duplicate row(s) you want to drop, then re-run the migration.

Note on multi-user setups

wacrm is intentionally single-tenant per WhatsApp number. RLS on conversations/messages is auth.uid() = user_id, so a second user physically cannot read messages routed to a different owner — two users sharing one number was never supported. If you need multiple humans handling the same inbox, run them under one shared account.

[0.2.0] — 2026-05-22

The Flows release. Adds a no-code, branching, button-driven WhatsApp conversation engine that runs alongside Automations. Also ships a 5-theme color picker in Settings and opens Flows to all users.

Added

Flows — branching chatbot conversations

  • Module + schema. New flows, flow_nodes, flow_runs, flow_run_events tables with partial unique indexes that enforce one active run per contact. Widened messages.content_type CHECK to accept 'interactive'; added interactive_reply_id column so the inbox can render button/list taps. (#112)
  • Runner engine. dispatchInboundToFlows parses every inbound webhook, decides whether the message is a reply on an active run or a fresh trigger, advances the state machine, and reports back to the webhook so consumed messages don't also fire automations. Idempotent on Meta's message_id. (#114)
  • No-code builder UI at /flows. Linear-list editor with per-node config forms, live validator, draft/active/archived status, and a 5-route REST API (GET/POST /api/flows, GET/PUT/DELETE /api/flows/[id], POST /api/flows/[id]/activate, GET /api/flows/[id]/runs, GET /api/flows/templates). (#115)
  • Templates + v1.5 node types. Three starter templates (Welcome menu, FAQ bot, Lead capture) cloneable from the New-flow dialog. Three new node types: collect_input (capture customer text into a variable), condition (branch on var / tag / contact field), set_tag (add or remove a tag). {{vars.X}} interpolation in send_message + collect_input prompts. Per-flow run-history viewer at /flows/[id]/runs. (#117)
  • Stale-run sweep cron at GET /api/flows/cron — marks runs past their configured timeout (default 24h) as timed_out so abandoned conversations free up the contact for new triggers. Reuses AUTOMATION_CRON_SECRET. (#114)

Color themes

  • 5 color themes (Violet default, Emerald, Cobalt, Amber, Rose) selectable from a new Appearance tab in Settings. CSS variables scoped under html[data-theme="..."], applied at runtime via dataset.theme, persisted to localStorage. Inline boot script in layout.tsx replays the choice before first paint so there's no flash of the default. (#132)
  • Theme tokenization sweep — every previously hard-coded violet-* Tailwind class replaced with primary tokens across ~49 files. Picking a non-violet theme now themes the whole app, not just the chrome. (#133)

Changed

Flows — soft-GA

  • Flows is now available to every authenticated user. The per-account beta gate is gone; the sidebar entry + page header carry a small "Beta" chip as the only remaining signal. (#134)
  • Editor UX:
    • Internal node_key + per-button/row reply_id identifiers hidden behind a per-node "Show advanced" disclosure. (#118)
    • send_list nodes can have multiple sections. (#119)
    • Collapsed node cards show a 1-line content preview per node type (text excerpt, button titles, condition summary, etc.). (#120)
    • Validation issues are clickable: jump to + flash the offending node. (#121)
    • Unsaved-changes "● Edited" indicator + beforeunload reload guard. (#122)
    • New-flow dialog actually widens to fit the 3 template cards (was capped at 384px by a baked-in sm:max-w-sm from shadcn). (#129, #131)
    • Validation panel pinned to the viewport bottom so activate-readiness follows the user as they scroll through nodes. (#130)

Engine reliability

  • Atomic execution_count increment via SECURITY DEFINER RPC — prevents lost counts when two webhooks start runs concurrently. Mirrors the automations engine pattern. (#124)
  • Preload all flow_nodes once per dispatch — one SELECT per inbound instead of one per advance-loop iteration. A 5-node auto-advance chain now costs 1 round trip, not 5. (#125)
  • Wasted re-read dropped after reprompt reset; loadActiveRun switched to defensive .limit(1) so a migration glitch producing duplicates can't crash dispatch. (#126)

Security

  • PII redacted from reply_received event payload — customer text is no longer persisted to flow_run_events.payload; only the length is. A collect_input prompt asking "what's your card number?" used to leave the PAN sitting in the events table. (#123)
  • Constant-time cron-secret compare on /api/flows/cron (crypto.timingSafeEqual) to close a theoretical timing-side-channel on the x-cron-secret header check. (#127)

Fixed

  • /flows no longer spuriously redirects to /dashboard when navigating in. Root cause: useAuth flipped loading: false before the profile fetch resolved. use-auth now exposes a separate profileLoading boolean. (#128)

Migration required

Apply, in order, against your Supabase project:

  1. supabase/migrations/010_flows.sql — Flows core tables, indexes, RLS policies, and the messages schema widening.
  2. supabase/migrations/011_profile_beta_features.sql — adds the profiles.beta_features column. Surviving for future betas; Flows no longer reads it.
  3. supabase/migrations/012_flows_increment_counter.sql — atomic counter RPC. Without this the engine still runs but flows.execution_count is racy.

Each migration is idempotent — safe to re-run if you're not sure whether you applied a previous one.

Removed

  • src/lib/flows/feature-flag.ts + its tests. Flows is open to all users; the profiles.beta_features column itself survives for future beta gates. (#134)

[0.1.1] — 2026-05-19

Added

  • Chat actions in the inbox: emoji reactions, reply-with-quote, and copy-text on individual messages. Hover on desktop, long-press on touch. Outbound reactions and replies forward to WhatsApp via the Cloud API; inbound reactions and swipe-replies from customers arrive through the webhook and appear in real time.

Migration required

  • Apply supabase/migrations/009_message_actions.sql to your Supabase project. It adds messages.reply_to_message_id and the new message_reactions table (with RLS and realtime). The migration is idempotent — safe to re-run.

Changed

  • The webhook no longer stores inbound customer reactions as fake text messages. They are written to message_reactions instead, so any custom queries that counted reactions as messages will need updating.

[0.1.0]

Initial template release. Core CRM: inbox, contacts, pipelines, broadcasts, automations (with a Wait-step cron drain), WhatsApp Cloud API integration, Supabase auth + RLS.