PR 3 of the multi-user accounts series. Lands the server-side endpoints the upcoming Members tab UI will call. Built on the helpers from #169 (`requireRole`, `getCurrentAccount`, `toErrorResponse`). API surface - GET /api/account — caller's account + role - PATCH /api/account — rename (admin+) - GET /api/account/members — roster; email visible to admin+ only - PATCH /api/account/members/[userId] — change role (admin+) - DELETE /api/account/members/[userId] — remove (admin+) - POST /api/account/transfer-ownership — atomic owner handoff (owner) Member-role writes can't ride on the regular `profiles_update` RLS policy (which only allows self-edits — admins can't modify a teammate's row that way). The three SECURITY DEFINER RPCs in the new migration are the scoped escape hatches: - `set_member_role(p_user_id, p_new_role)` — admin+ only. Blocks promotion to / demotion from owner (use transfer instead) and self-targeting. - `remove_account_member(p_user_id)` — admin+ only. Creates a fresh personal account for the removed user and reassigns their profile as its 'owner' — mirror of the signup flow, so they keep their login and just "start over". Blocks removing the owner or self. - `transfer_account_ownership(p_new_owner_user_id)` — owner only. Atomically demotes current owner → admin, promotes target → owner, and points accounts.owner_user_id at the new owner. Demote-first ordering means the temporary "zero owners" state is never visible. Error contract - Each RPC raises with SQLSTATE 42501 (forbidden) or 22023 (bad input) and a human-readable RAISE message. The route handlers map these onto 403 / 400 respectively; anything else collapses to a generic 500. - The TS guards (e.g. `requireRole('owner')` on the transfer route, `isAccountRole(role)` on the PATCH route) are belt-and-braces — they fail fast before the Supabase round trip but the RPCs would catch the same violations regardless. Field visibility - `GET /api/account/members` returns `email` only when the caller is admin+. Agents and viewers see name + avatar + role + joined date — matches the planning decision so a viewer can't harvest the team's email list. Verification - `npm run lint` → 0 errors (same 20 pre-existing warnings) - `npm run typecheck` → clean - `npm test` → 366/366 pass - `npm run build` → succeeds; the 4 new routes register as ƒ (dynamic — they all use `next/headers` cookies via the SSR client) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 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 ofauth.uid() = user_id. Theuser_idcolumns 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 byUNIQUE(account_id). flow_runsidempotency 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 asowner.
Added
- Account & member management API — server-side endpoints
for the upcoming Members tab UI. All routes are role-gated and
return Supabase-RLS-scoped data.
GET /api/account— caller's account + role. Any member.PATCH /api/account— rename the account. Admin+.GET /api/account/members— list members. Email visible to admin+ only; agents/viewers see name + avatar + role + joined date.PATCH /api/account/members/[userId]— change a member's role. Admin+. Owner promotion/demotion goes through the transfer endpoint instead.DELETE /api/account/members/[userId]— remove a member. Admin+. The removed user keeps their login and is moved to a freshly-created personal account (mirror of the signup flow).POST /api/account/transfer-ownership— owner only. Atomic swap with the named member.
Migration required
Apply against your Supabase project before deploying this version:
supabase/migrations/017_account_sharing.sql— introduces theaccountsandaccount_invitationstables plus anaccount_role_enumtype; addsaccount_idto 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 roleownerand every existing row of theirs is linked to that account.supabase/migrations/018_account_member_rpcs.sql— adds threeSECURITY DEFINERRPCs (set_member_role,remove_account_member,transfer_account_ownership) that back the member-management API. They self-check the caller's role and raise SQLSTATE42501/22023on forbidden / bad input so the API layer can map cleanly to 403 / 400. Idempotent.
[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_mediaflow 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 newflow-mediaSupabase 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 assend_message. (#156)
Migration required
Apply against your Supabase project before deploying this version:
supabase/migrations/016_flow_media.sql— does two things:- Adds
'send_media'to theflow_nodes.node_typeCHECK constraint. Without this thesend_medianode fails to save with a constraint violation. - Creates the public
flow-mediaSupabase 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 theavatarsbucket from migration 008 — the bucket is public so Meta can fetch the URL without credentials.
- Adds
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 errorsPGRST116for 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/confignow returns 409 when another user has already claimed the number, the webhook lookup distinguishes 0 rows from ≥2 rows and logs the conflictinguser_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— addsUNIQUE(phone_number_id)towhatsapp_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,
DELETEthe 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_eventstables with partial unique indexes that enforce one active run per contact. Widenedmessages.content_typeCHECK to accept'interactive'; addedinteractive_reply_idcolumn so the inbox can render button/list taps. (#112) - Runner engine.
dispatchInboundToFlowsparses 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'smessage_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) astimed_outso abandoned conversations free up the contact for new triggers. ReusesAUTOMATION_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 viadataset.theme, persisted tolocalStorage. Inline boot script inlayout.tsxreplays 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 withprimarytokens 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/rowreply_ididentifiers hidden behind a per-node "Show advanced" disclosure. (#118) send_listnodes 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 +
beforeunloadreload guard. (#122) - New-flow dialog actually widens to fit the 3 template cards
(was capped at 384px by a baked-in
sm:max-w-smfrom shadcn). (#129, #131) - Validation panel pinned to the viewport bottom so activate-readiness follows the user as they scroll through nodes. (#130)
- Internal
Engine reliability
- Atomic
execution_countincrement 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;
loadActiveRunswitched to defensive.limit(1)so a migration glitch producing duplicates can't crash dispatch. (#126)
Security
- PII redacted from
reply_receivedevent payload — customer text is no longer persisted toflow_run_events.payload; only the length is. Acollect_inputprompt 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 thex-cron-secretheader check. (#127)
Fixed
/flowsno longer spuriously redirects to/dashboardwhen navigating in. Root cause:useAuthflippedloading: falsebefore the profile fetch resolved.use-authnow exposes a separateprofileLoadingboolean. (#128)
Migration required
Apply, in order, against your Supabase project:
supabase/migrations/010_flows.sql— Flows core tables, indexes, RLS policies, and themessagesschema widening.supabase/migrations/011_profile_beta_features.sql— adds theprofiles.beta_featurescolumn. Surviving for future betas; Flows no longer reads it.supabase/migrations/012_flows_increment_counter.sql— atomic counter RPC. Without this the engine still runs butflows.execution_countis 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; theprofiles.beta_featurescolumn 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.sqlto your Supabase project. It addsmessages.reply_to_message_idand the newmessage_reactionstable (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_reactionsinstead, 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.