diff --git a/CHANGELOG.md b/CHANGELOG.md
index 265b8cc..07547d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,21 @@ as the sole owner of their own account and sees identical data.
- **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
diff --git a/src/app/(dashboard)/automations/page.tsx b/src/app/(dashboard)/automations/page.tsx
index dadead5..ed861d3 100644
--- a/src/app/(dashboard)/automations/page.tsx
+++ b/src/app/(dashboard)/automations/page.tsx
@@ -19,6 +19,7 @@ import {
} from "lucide-react"
import { createClient } from "@/lib/supabase/client"
+import { useCan } from "@/hooks/use-can"
import type { Automation } from "@/types"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
@@ -55,8 +56,12 @@ const TEMPLATE_ICON: Record = {
follow_up_reminder: PhoneCall,
}
+const READ_ONLY_TITLE =
+ "Read-only — your role can't create automations"
+
export default function AutomationsPage() {
const router = useRouter()
+ const canCreate = useCan("send-messages")
const [automations, setAutomations] = useState(null)
const [error, setError] = useState(null)
const [pendingDelete, setPendingDelete] = useState(null)
@@ -164,6 +169,8 @@ export default function AutomationsPage() {
@@ -326,7 +343,12 @@ function EmptyState({ onCreate }: { onCreate: () => void }) {
bot. Customers tap buttons; the bot routes them to the right answer (or
the right agent).
-
+
Create your first flow
diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/(dashboard)/pipelines/page.tsx
index 7d0a539..24a95e2 100644
--- a/src/app/(dashboard)/pipelines/page.tsx
+++ b/src/app/(dashboard)/pipelines/page.tsx
@@ -26,6 +26,14 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { GitBranch, Plus, ChevronDown, Settings } from "lucide-react";
import { toast } from "sonner";
+import { useCan } from "@/hooks/use-can";
+
+// Pipeline creation is admin-class (it's a settings-tier write
+// under the new RLS), but deal creation is operational and only
+// requires agent+. The two CTAs gate on different capabilities.
+const PIPELINE_READ_ONLY_TITLE =
+ "Read-only — your role can't create pipelines";
+const DEAL_READ_ONLY_TITLE = "Read-only — your role can't create deals";
// Spec-defined seed — name and color per the product spec.
const SPEC_DEFAULT_STAGES = [
@@ -38,6 +46,8 @@ const SPEC_DEFAULT_STAGES = [
export default function PipelinesPage() {
const supabase = createClient();
+ const canEditSettings = useCan("edit-settings");
+ const canCreateDeals = useCan("send-messages");
const [pipelines, setPipelines] = useState([]);
const [selectedPipelineId, setSelectedPipelineId] = useState("");
@@ -349,6 +359,8 @@ export default function PipelinesPage() {
setNewPipelineOpen(true)}
+ disabled={!canEditSettings}
+ title={canEditSettings ? undefined : PIPELINE_READ_ONLY_TITLE}
className="border-slate-700 bg-slate-900 text-slate-200 hover:bg-slate-800"
>
@@ -356,7 +368,10 @@ export default function PipelinesPage() {
handleAddDeal()}
- disabled={!selectedPipelineId || stages.length === 0}
+ disabled={
+ !canCreateDeals || !selectedPipelineId || stages.length === 0
+ }
+ title={canCreateDeals ? undefined : DEAL_READ_ONLY_TITLE}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
@@ -377,6 +392,8 @@ export default function PipelinesPage() {
setNewPipelineOpen(true)}
+ disabled={!canEditSettings}
+ title={canEditSettings ? undefined : PIPELINE_READ_ONLY_TITLE}
className="mt-4 bg-primary text-primary-foreground hover:bg-primary/90"
>
diff --git a/src/components/inbox/message-composer.tsx b/src/components/inbox/message-composer.tsx
index 06de109..385c64e 100644
--- a/src/components/inbox/message-composer.tsx
+++ b/src/components/inbox/message-composer.tsx
@@ -3,6 +3,7 @@
import { useState, useRef, useCallback, KeyboardEvent } from "react";
import { Send, LayoutTemplate } from "lucide-react";
import { Button } from "@/components/ui/button";
+import { useCan } from "@/hooks/use-can";
import { cn } from "@/lib/utils";
import { ReplyQuote } from "./reply-quote";
@@ -33,6 +34,11 @@ export function MessageComposer({
const [text, setText] = useState("");
const [sending, setSending] = useState(false);
const textareaRef = useRef(null);
+ // Viewers (read-only role) can browse the inbox but never send.
+ // For solo users this is always true — single-owner accounts pass
+ // every capability — so the disabled branch is a no-op there.
+ const canSend = useCan("send-messages");
+ const readOnly = !canSend;
const adjustHeight = useCallback(() => {
const el = textareaRef.current;
@@ -110,7 +116,8 @@ export function MessageComposer({
size="sm"
className="h-9 w-9 shrink-0 p-0 text-slate-400 hover:text-white"
onClick={onOpenTemplates}
- title="Send template"
+ disabled={readOnly}
+ title={readOnly ? "Read-only — your role can't send messages" : "Send template"}
>
@@ -121,23 +128,27 @@ export function MessageComposer({
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={
- sessionExpired
- ? "Session expired - use a template"
- : "Type a message... (Shift+Enter for new line)"
+ readOnly
+ ? "Read-only — viewers can browse but not reply"
+ : sessionExpired
+ ? "Session expired - use a template"
+ : "Type a message... (Shift+Enter for new line)"
}
- disabled={sessionExpired}
+ disabled={sessionExpired || readOnly}
rows={1}
+ title={readOnly ? "Read-only — your role can't send messages" : undefined}
className={cn(
"flex-1 resize-none rounded-xl border border-slate-700 bg-slate-800 px-4 py-2.5 text-sm text-white placeholder-slate-500 outline-none transition-colors focus:border-primary/50",
- sessionExpired && "cursor-not-allowed opacity-50"
+ (sessionExpired || readOnly) && "cursor-not-allowed opacity-50"
)}
/>
diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx
index d550745..316d4bd 100644
--- a/src/components/layout/sidebar.tsx
+++ b/src/components/layout/sidebar.tsx
@@ -17,6 +17,7 @@ import {
Settings,
LogOut,
User,
+ UsersRound,
X,
} from "lucide-react";
import {
@@ -63,10 +64,17 @@ interface SidebarProps {
onClose?: () => void;
}
+// Same flag key the Members tab and Settings page use. Flip per
+// profile via Supabase Studio to dogfood the multi-user surface.
+const ACCOUNT_SHARING_FLAG = "account_sharing";
+
export function Sidebar({ open = false, onClose }: SidebarProps) {
const pathname = usePathname();
- const { profile, signOut } = useAuth();
+ const { profile, account, accountRole, signOut } = useAuth();
const totalUnread = useTotalUnread();
+ const accountSharingEnabled = !!profile?.beta_features?.includes(
+ ACCOUNT_SHARING_FLAG,
+ );
// Close the drawer when route changes — users opened it to navigate,
// so once they pick a destination the drawer should get out of the way.
@@ -216,6 +224,23 @@ export function Sidebar({ open = false, onClose }: SidebarProps) {
{/* User section */}
+ {/* Account name display — only surfaced when the user is
+ opted into the account_sharing beta flag. For solo
+ users (the default) the account is named after them,
+ so showing it here would just duplicate the user name
+ below. Once the flag is on the user is at minimum
+ aware of which shared account they're acting in. */}
+ {accountSharingEnabled && account?.name ? (
+