Status: Canonical source of truth for roles, access,
and permissions.
Version: 2.0
Last Updated: 2026-06-15
Backend: Supabase (project
gokslnrvxqledagcwghq), enforced by Row-Level
Security.
Apps: Sales OS (/system), CRM
(/crm), shared shell (/app: auth, launcher,
admin/master consoles).
organisation (org). Every company user carries that
company’s org_id. Data is isolated per company.| Role | Meaning |
|---|---|
super_duper_admin |
Platform Master. Above all companies. Provisions companies and their super admins. |
super_admin |
Company Owner. Full control of one company. |
admin |
Sales Manager. Runs a team (subordinates). |
admin_m |
Management. Company-wide oversight, view-only. |
sdr |
Senior Rep / Team Lead. Works own deals; sees (not edits) team. |
account_executive |
Rep. Works own deals only. |
public |
Evaluator. Sales OS only, own data, no company, CRM locked. |
super_duper_admin (Platform Master)
└── super_admin (Company Administrator)
├── admin (Team Manager)
│ ├── sdr (Sales Dev Rep - team visibility)
│ └── account_executive (Individual Contributor)
└── admin_m (Manager - Read Only)
SDR vs Account Executive: both write only their own records and move their own leads. The SDR additionally sees (read-only) their team’s records to aid the admin.
Tables: leads,
opportunities, deals, accounts,
contacts, activities, tasks
| Role | Can VIEW | Can CREATE / EDIT / DELETE | Move own leads through stages |
|---|---|---|---|
| super_duper_admin | all companies | all companies | n/a |
| super_admin | whole company | whole company | yes |
| admin | self + team (full downline) | self + team (full downline) | yes (own + team) |
| admin_m | whole company | nothing — view only | no |
| sdr | self + team (full downline) | self only | yes (own only) |
| account_executive | self only | self only | yes (own only) |
| public | self (Sales OS only) | self (Sales OS only) | yes (own only) |
leads: lead_owner_idopportunities: owner_iddeals: assigned_toaccounts: account_owner_idcontacts: contact_owner_idactivities: owner_idtasks: assignee_idTables: weekly_reports and all derived
pipeline/forecast/win-loss views
| Role | View | Create/Edit/Delete reports | Email a report (see §7) |
|---|---|---|---|
| super_duper_admin | all | yes | yes |
| super_admin | whole company | yes | yes |
| admin | self + team | yes (own + team) | yes |
| admin_m | whole company | no — view only | yes |
| sdr | self + team | no — view only | own report to own manager only |
| account_executive | self | no | own report to own manager only |
| public | self | no | no |
Key Distinction: Closing one’s own deal (won/lost status on a record you own) is a normal data action under §3 — it is not restricted by this report-layer rule. The restriction here applies to the aggregate report/forecast layer.
Effect: - admin_m can VIEW all reports company-wide but cannot CREATE/EDIT/DELETE - sdr can VIEW own + team reports but cannot CREATE/EDIT/DELETE - account_executive cannot create/edit aggregate reports (separate from closing own deals)
| Actor | Can create & invite | Can edit | Can delete / deactivate | Can send login / reset link |
|---|---|---|---|---|
| super_duper_admin | a new company + its super_admin |
any user, any company | any | any |
| super_admin | admin, admin_m, sdr,
account_executive (own company) |
own-company users | own-company users | own-company users |
| admin | sdr, account_executive (their own
team) |
their team | their team | their team |
| admin_m | none | none | none | none |
| sdr | none | none | none | none |
| account_executive | none | none | none | none |
| public | none | none | none | none |
Inheritance Rules: - Every user an actor creates
inherits the actor’s org_id automatically - For users
created by admin: manager_id = the admin - No
company ID is ever entered by hand - New user receives invite link with
redirectTo = https://shaamelz.com/app/set-password.html
generateLink type
recovery)https://shaamelz.com/app/set-password.htmlImplementation: 1. Edge Function generates PKCE
recovery link with redirectTo 2. Link expires after 3600
seconds (1 hour, configurable in Supabase) 3. Landing page
(set-password.html) uses two-stage
activation: - Stage 1: Detect token but don’t consume (shows
“Continue to Set Password” button) - Stage 2: User clicks button → token
exchanged via exchangeCodeForSession() 4.
Why: Email scanners pre-fetch links but don’t click
buttons → token not consumed by scanner 5. User sets password →
auto-routed to workspace based on role + crm_enabled
Security: - Single-use tokens (consumed on first exchange) - PKCE flow (code exchange, not direct tokens in URL) - Clear error messages for expired/used links - “Request New Link” recovery flow
Authorised actors (admin, admin_m,
super_admin, super_duper_admin) can email a
report or dataset — leads, pipeline, forecast, closed-won, closed-lost —
directly from the screen it’s shown on.
Recipients: - To: the company’s
Management (admin_m) via
organisations.contact_email - Cc: the
subordinate who owns the data in that report - Optionally Cc the
sender
Authorization: - sdr and
account_executive may email only their own report,
and only to their own manager - Report data automatically filtered by
RLS (app_manages) — cannot email data user doesn’t have access to
Implementation Status: - Edge Function authorization: ✅ Complete - Email sending: 🟡 Stub (integration with SendGrid/Resend pending)
Sales OS and the CRM are not two systems — they are two
lenses over the same records. The Sales OS is the
diagnostic/qualification lens; the CRM is the full-lifecycle management
lens. Both read and write the same leads /
opportunities / deals, which already carry the
diagnostic state (ignite_data, attract_data,
probe_data, execute_data).
crm_enabled decides how many lenses a user can
open — it is an access flag, never a second copy of the data.
crm_enabled = false (casual/public) → Sales OS lens
onlycrm_enabled = true (company users) → both lenses
availablecrm_enabled to true simply unlocks the CRM lens onto the
records they already created. Nothing copies.Applies to ALL users (public, company users, all roles) working deals:
ignite_data, attract_data,
probe_data, execute_data)The diagnostic determines eligibility; movement is configurable:
diagnostic_config table)One front door — shaamelz.com: Log in /
Sign up only. The system routes after auth; users never type app
URLs.
Flow: 1. User self-registers at
https://shaamelz.com (Supabase auth UI) 2. User parked
under the NA holding company: -
role = 'public' - crm_enabled = false -
org_id = NA company UUID -
account_type = 'casual' 3. NA holds only casual
users — never real company users 4. Supabase sends set-password email
automatically (no manual master invite for this tier - keep free sign-up
frictionless) 5. User opens link → sets and confirms password →
auto-logged in → routed into Sales OS 6.
Platform notification: On successful registration, send
Platform Master (muhammad.shaamel@gmail.com) a signup
notification: - User’s email - Company: NA - Timestamp - Never
the password (passwords are hashed and must never be
transmitted) 7. In the app: Sales OS available, CRM
button/card disabled with popup: > “You need to register under a
company to use the CRM. To request access, email platform admin muhammad.shaamel@gmail.com.”
Isolation: - Casual users share the NA org but RLS
isolates them: public role sees only records where
owner_col = auth.uid() - They cannot see each other’s data
despite same org_id - If later added to a real company:
org_id changes, crm_enabled flips to true →
seamless upgrade
Flow: 1. super_duper_admin goes to Master Console →
“Provision New Company” 2. Enter: company name, address, contact
email/phone + super admin name/email 3. Backend: - Creates
organisations record (with slug) - Creates
super_admin user tagged to that org - Generates invite link
(PKCE recovery type) 4. Invite link emailed or shown to master for
manual distribution 5. Invited super admin opens link → sets password →
lands as super_admin of that org (never defaulted to
public)
Critical: Never redirect on a half-loaded state — await FULL profile including role, org_id, crm_enabled before any routing decision.
Router Logic (/app/launcher.html):
await session = getSession()
await profile = getProfile(session.user.id) // role, org_id, crm_enabled, is_active
if (profile.role === 'super_duper_admin') → /app/master-console.html
if (profile.role === 'super_admin') → /app/company-dashboard.html
if (profile.role IN ['admin','admin_m','sdr','account_executive']) {
if (profile.crm_enabled) → /crm/index.html
else → /system/index.html
}
if (profile.role === 'public') → /system/index.html (CRM locked)
Terminal Error Pages: If unauthorized, show terminal error (no redirect loop). User sees “Access Denied” with “Back to Launcher” link — page does NOT auto-redirect again.
All workspaces must show: - Company name (from
organisations.name via profile.org_id) - User
name + role badge - Logout button
Current Status: - ✅ Launcher shows company name - 🟡 CRM and Sales OS workspaces: company name missing (minor UX issue, non-blocking)
Flow: 1. super_admin or admin navigates to user
management UI 2. Clicks “Add Team Member” → modal opens 3. Fills: name,
email, role, manager (dropdown), crm_enabled checkbox 4. Clicks “Send
Invitation” 5. Backend: - Creates user in auth.users with
random password - Creates profile in public.users with
inherited org_id - Sets manager_id (for
admin-created users) - Generates invite link → returns to UI 6. UI shows
invite link with “Copy Link” button 7. Admin sends link to new user
(email, Slack, etc.) 8. New user follows §6 invite flow
Already deployed in 001_rbac.sql:
app_role() -- Returns current user's role
app_org() -- Returns current user's org_id
app_manages(target_user_id UUID) -- TRUE if target is current user or in their downline (recursive)
app_manages Logic: - Recursive CTE walks
manager_id chain upward from target - Returns TRUE if
auth.uid() appears in the chain - Used for hierarchical
permissions (admin, admin_m, sdr can read their downline)
Tables: leads,
opportunities, deals, accounts,
contacts, activities, tasks
Replace owner_col / org_col with each
table’s real column names (see §3).
-- READ Policy
CREATE POLICY {table}_select ON public.{table}
FOR SELECT
USING (
app_role() = 'super_duper_admin'
OR (org_col = app_org() AND (
app_role() IN ('super_admin','admin_m')
OR app_manages(owner_col)))
);
-- WRITE Policy (INSERT/UPDATE/DELETE)
CREATE POLICY {table}_insert ON public.{table}
FOR INSERT
WITH CHECK (
app_role() = 'super_duper_admin'
OR (org_col = app_org() AND (
app_role() = 'super_admin'
OR (app_role() = 'admin' AND app_manages(owner_col))
OR (app_role() IN ('sdr','account_executive','public')
AND owner_col = auth.uid())))
);
-- Similar for UPDATE/DELETE with USING instead of WITH CHECK
Effect: - super_duper_admin: bypasses all checks - super_admin, admin_m: see whole company - admin: sees self + downline, writes self + downline - sdr: sees self + downline, writes self only - account_executive, public: see and write self only
Tables: weekly_reports, forecast
tables
Same READ policy as core data, but WRITE excludes sdr and admin_m:
-- WRITE Policy (sdr and admin_m excluded)
CREATE POLICY weekly_reports_insert ON public.weekly_reports
FOR INSERT
WITH CHECK (
app_role() = 'super_duper_admin'
OR (org_col = app_org() AND (
app_role() = 'super_admin'
OR (app_role() = 'admin' AND app_manages(owner_col))
OR (app_role() = 'account_executive' AND owner_col = auth.uid())))
);
Config tables (configs): org-wide read,
super_admin/master write
Platform tables (app_settings): master /
super_admin only
deal_states (no own owner/org): gate
access via its parent deal
users: See existing policy in
001_rbac.sql (users can read managed users, write needs
permission)
| Migration | Description | Status |
|---|---|---|
| 001_rbac.sql | Initial RLS + helper functions | ✅ Applied |
| 002_fix_configs_policies.sql | Fixed configs table policies | ✅ Applied |
| 003_cleanup_old_policies.sql | Removed manual dashboard policies | ✅ Applied |
| 004_onboarding_flow.sql | Company contact fields, initial sdr read-only (WRONG) | ✅ Applied |
| 005_fix_sdr_permissions.sql | Fixed sdr to WRITE own + READ team | ✅ Applied |
Current schema version: 005
Next: 006_diagnostic_tables.sql (when diagnostic-gated pipeline is implemented)
| Function | Purpose | Authorization | Status |
|---|---|---|---|
| provision-company | Create org + super_admin | super_duper_admin only | ✅ Deployed |
| generate-invite-link | Invite team member | super_admin, admin | ✅ Deployed |
| generate-login-link | Send password reset | super_admin (own org) | ✅ Deployed |
| reset-user-password | Reset password (master) | super_duper_admin only | ✅ Deployed |
| send-team-report | Email team report | super_admin, admin, admin_m, sdr | 🟡 Stub (auth works) |
All functions: - Verify JWT via service_role admin API - Check caller authorization before executing - Return clear errors on failure - Use service_role key for admin operations
Authentication & Session: - [ ] Login routes cleanly by role — no redirect loop - [ ] Session persists across page reloads - [ ] Logout works from any page - [ ] No redirect loops on any page
Casual / Public User Flow: - [ ] Public user: Sales OS only, CRM locked with upgrade message - [ ] Self-registration creates user under NA company - [ ] Public users isolated from each other (cannot see each other’s data) - [ ] Platform master receives signup notification (email only, no password)
Company Provisioning: - [ ] Invited super_admin
lands as super_admin of their own company (not
public) - [ ] Super_admin sees company profile (name,
address, contact) - [ ] Super_admin sees only their company data
(cross-company isolation)
Team Building: - [ ] Invited team member inherits company automatically; no company ID typed - [ ] Invite link displays with copy-to-clipboard - [ ] Invite link survives email scanner pre-fetch (two-stage activation) - [ ] Password setup routes to correct workspace
Data Access - Core Tables: - [ ] AE: creates and stage-advances only their own leads; sees only their own data - [ ] SDR: creates/stage-advances their own leads; sees (read-only) their team; cannot edit team records - [ ] SDR: cannot manage users; read-only on reports - [ ] admin: full CRUD on self + team; can create/edit/delete/invite their team - [ ] admin_m: views everything company-wide, edits nothing - [ ] super_admin: full company control + all company user management
Data Access - Reports: - [ ] SDR can view reports (own + team) but cannot create/edit - [ ] admin_m can view reports company-wide but cannot create/edit - [ ] account_executive cannot create aggregate reports (can close own deals) - [ ] admin can email reports to admin_m + owning subordinate
Cross-Company Isolation: - [ ] Company A users never see Company B data - [ ] Public users (NA company) cannot see each other’s data
Diagnostic-Gated Pipeline (when implemented): - [ ] Pipeline stage moves blocked unless stage’s IGNITE diagnostic is satisfied - [ ] Diagnostic state stored on deal record - [ ] Qualification checked on every stage move
No separate tables: Both lenses use same tables
(leads, opportunities, deals)
Diagnostic columns on deals:
ALTER TABLE public.deals ADD COLUMN IF NOT EXISTS ignite_data JSONB;
ALTER TABLE public.deals ADD COLUMN IF NOT EXISTS attract_data JSONB;
ALTER TABLE public.deals ADD COLUMN IF NOT EXISTS probe_data JSONB;
ALTER TABLE public.deals ADD COLUMN IF NOT EXISTS execute_data JSONB;
Structure:
{
"completed": true,
"score": 78,
"passed": true,
"completed_at": "2026-06-15T10:30:00Z",
"responses": [
{"question": "I1", "answer": "High cost", "score": 8},
{"question": "G1", "answer": "Strong interest", "score": 9}
]
}
Sales OS (/system/index.html): - Shows
IGNITE diagnostic interface - Focuses on qualification questions -
Available to all roles (including public)
CRM (/crm/index.html): - Shows full
pipeline with accounts/contacts - Uses diagnostic state from same deals
- Only available if crm_enabled = true
User with crm_enabled = true: - Can
open both /system and /crm - Sees same deals
in both places - Diagnostic completed in Sales OS → visible in CRM -
Deal moved in CRM → diagnostic state checked
auth.uid()
determines user identitypublic.users
table determines role/orgpublic.users table has RLS to prevent role
escalation| Capability | super_duper | super_admin | admin | admin_m | sdr | AE | public |
|---|---|---|---|---|---|---|---|
| See all orgs | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| See own org (all) | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| See downline | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Edit own data | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
| Edit downline data | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Edit reports | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Invite users | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Access CRM | ✅ | ✅ | ✅* | ✅* | ✅* | ✅* | ❌ |
| Access Sales OS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
*if crm_enabled=true
Core Data (leads, opps, deals, etc.): - READ: super_duper OR (own org AND (super_admin/admin_m OR manages owner)) - WRITE: super_duper OR (own org AND (super_admin OR (admin manages) OR (sdr/AE/public owns)))
Reports/Forecasts: - READ: same as core data - WRITE: super_duper OR (own org AND (super_admin OR (admin manages) OR (AE owns))) - NOTE: sdr + admin_m + public excluded from WRITE
Users: - READ: super_duper OR (own org AND (super_admin OR manages target)) - WRITE: super_duper OR (own org AND (super_admin OR (admin manages)))
END OF SPECIFICATION
This is the single, authoritative source of truth for IGNITE-APEX access control.