mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
PR 9 of the multi-user accounts series — the final piece. Visual
polish so non-owners see consistent disabled states across the
app and so every user always knows which account they're acting in.
Sidebar
- New account-name strip above the user dropdown, gated behind
the `account_sharing` beta flag (same flag the Members tab
from PR 6 reads). For solo users (the default) the account is
named after them, so showing it would just duplicate the user
name below — flag-gating keeps the original layout untouched
for single-user installs.
- When the caller's role is admin/agent/viewer (not owner), the
role is surfaced as a small uppercase chip alongside the
account name so they have an unambiguous signal of their
permission level without opening Settings.
- Nav items stay always-visible per the planning doc's
"show-but-disable" decision — hiding nav rows by role would
make the app feel broken.
Inbox composer
- Send button + Templates button + textarea are disabled for
viewers with a "Read-only — viewers can browse but not reply"
placeholder and tooltip. The existing session-expired branch
still wins when it applies (session-expired is the more
specific condition and surfaces template-only sending).
List-page create buttons
- broadcasts/page.tsx: "New Broadcast" CTA (header + empty
state).
- automations/page.tsx: "Create Automation" CTA.
- flows/page.tsx: "New flow" CTA (header + empty-state).
- contacts/page.tsx: "Add Contact" + "Import" CTAs.
- pipelines/page.tsx: "Add Pipeline" (gates on edit-settings —
pipelines are settings-class under the 017 RLS) and "Add
Deal" (gates on send-messages).
All use `useCan(...)` from PR 5. Disabled buttons get a tooltip
explaining why (`title="Read-only — your role can't ..."`).
Solo users see no visual change — single-owner accounts pass
every capability check.
Why show-but-disable, not hide
- A viewer scanning the app should still see what's possible
in the product, both to understand what they can ask for an
upgrade for and to avoid the UI feeling silently broken
("the New button just disappeared?"). The tooltip carries
the explanation cheaply.
Verification
- `npm run lint` → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test` → 366/366 pass
- `npm run build` → succeeds
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
400 lines
18 KiB
Markdown
400 lines
18 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`.
|
|
|
|
### Changed
|
|
|
|
- **Role-aware UI gating across the app.** The inbox composer's
|
|
send button + textarea, the "New broadcast / automation / flow"
|
|
buttons, the "Add pipeline / deal" buttons, and the "Add /
|
|
Import contact" buttons are now disabled-with-tooltip for
|
|
viewers (and for agents on settings-class actions). Choice:
|
|
show-but-disable rather than hide, so the UI never feels
|
|
silently broken to a teammate looking at a feature they don't
|
|
yet have permission for.
|
|
- **Sidebar surfaces the active account** above the user info
|
|
when the `account_sharing` beta flag is on. Solo users keep
|
|
the original layout (their account is named after them, so
|
|
duplicating it would just add visual noise).
|
|
|
|
### Fixed
|
|
|
|
- **Inbound WhatsApp messages now land in the shared inbox.** The
|
|
webhook + automations + flows engines used to route inbound
|
|
events by `user_id`, which after the 017 migration only matched
|
|
the WhatsApp config owner's automations / flows — teammates'
|
|
rules never fired. PR 8 of the multi-user series flips every
|
|
lookup to `account_id` so any member of the account sees the
|
|
inbound message and any teammate's automation or flow can react
|
|
to it. Also fixes incipient NOT NULL violations on
|
|
`automation_logs`, `automation_pending_executions`, `flow_runs`,
|
|
and `deals` — those tables gained `account_id NOT NULL` in 017
|
|
but the engines hadn't yet been updated to populate it.
|
|
|
|
### 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.
|
|
- **Invitation API + redeem flow** — the no-email, link-only
|
|
invite path. Backend is complete; the Members tab UI that
|
|
drives it lands in a follow-up.
|
|
- `GET /api/account/invitations` — list outstanding (admin+).
|
|
- `POST /api/account/invitations` — create an invite, returns
|
|
the plaintext token + share URL **exactly once** (we store
|
|
only the SHA-256 hash on the row). Body
|
|
`{ role, expiresInDays?, label? }`. Admin+.
|
|
- `DELETE /api/account/invitations/[id]` — revoke (admin+).
|
|
- `GET /api/invitations/[token]/peek` — public, per-IP
|
|
rate-limited. Returns `{ ok, account_name, role, expires_at }`
|
|
or `{ ok: false, reason }` so the join page can render
|
|
"You're being invited to <Account> as <Role>".
|
|
- `POST /api/invitations/[token]/redeem` — authenticated.
|
|
Atomically moves the caller's profile to the inviter's
|
|
account and cleans up the orphan personal account. Refuses
|
|
with 409 if the caller's current account already contains
|
|
domain data (no silent data loss).
|
|
|
|
### 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.
|
|
- `supabase/migrations/018_account_member_rpcs.sql` — adds three
|
|
`SECURITY DEFINER` RPCs (`set_member_role`,
|
|
`remove_account_member`, `transfer_account_ownership`) that
|
|
back the member-management API. They self-check the caller's
|
|
role and raise SQLSTATE `42501` / `22023` on forbidden / bad
|
|
input so the API layer can map cleanly to 403 / 400.
|
|
Idempotent.
|
|
- `supabase/migrations/019_invitation_rpcs.sql` — adds two
|
|
`SECURITY DEFINER` RPCs: `peek_invitation` (anonymous read by
|
|
token hash, returns a fixed-shape JSON envelope) and
|
|
`redeem_invitation` (authenticated atomic move + orphan
|
|
cleanup, with a domain-data safety check). Both bypass the
|
|
RLS that would otherwise block their reads/writes. 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_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.
|