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

321 lines
14 KiB
Markdown

# 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](https://keepachangelog.com/en/1.1.0/).
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](https://github.com/ArnasDon/wacrm/pull/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_id`s, and a new DB constraint
(`UNIQUE(phone_number_id)`) prevents the bad state at the storage
layer. Reported in
[#136](https://github.com/ArnasDon/wacrm/issues/136), fixed in
[#143](https://github.com/ArnasDon/wacrm/pull/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:
```sql
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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/134))
- **Editor UX**:
- Internal `node_key` + per-button/row `reply_id` identifiers
hidden behind a per-node "Show advanced" disclosure.
([#118](https://github.com/ArnasDon/wacrm/pull/118))
- `send_list` nodes can have multiple sections.
([#119](https://github.com/ArnasDon/wacrm/pull/119))
- Collapsed node cards show a 1-line content preview per node
type (text excerpt, button titles, condition summary, etc.).
([#120](https://github.com/ArnasDon/wacrm/pull/120))
- Validation issues are clickable: jump to + flash the offending
node.
([#121](https://github.com/ArnasDon/wacrm/pull/121))
- Unsaved-changes "● Edited" indicator + `beforeunload` reload
guard.
([#122](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/129),
[#131](https://github.com/ArnasDon/wacrm/pull/131))
- Validation panel pinned to the viewport bottom so
activate-readiness follows the user as they scroll through nodes.
([#130](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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](https://github.com/ArnasDon/wacrm/pull/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.