link,[object Object]
Skip to content

Blur Policy (Listing Field Access Control)

Purpose: Document exactly which fields are blurred, who can view them, and how the rule is applied in DB and UI. Audience: Developer, Admin, AI Agent Prerequisites: Linked Supabase project; up-to-date cloud schema.

Model and components

  • Table: public.blurred_fields

    • Columns: id uuid, field_key text, field_name text, field_description text?, is_blurred_for_unauthenticated boolean default false, is_blurred_for_free boolean default false, is_blurred_for_starter boolean default false, is_blurred_for_pro boolean default false, created_at timestamptz default now(), updated_at timestamptz default now()
  • Function (RPC): public.can_view_field(p_listing uuid, p_field_key text, p_user uuid default auth.uid())

    • Admin/owner bypass; if blurred_fields has no entry the field is visible; otherwise access is decided by subscription level.
sql
-- Lines 12-66 from supabase/migrations/20250813134500_can_view_field.sql
create or replace function public.can_view_field(
  p_listing uuid,
  p_field_key text,
  p_user uuid default auth.uid()
)
returns boolean
language plpgsql
stable
as $$
  ...
  if v_level = 'pro' then
    return not coalesce(bf.is_blurred_for_pro, false);
  elsif v_level = 'starter' then
    return not coalesce(bf.is_blurred_for_starter, false);
  else
    return not coalesce(bf.is_blurred_for_free, true);
  end if;
end;
$$;
  • Secure view: public.listing_view_secure
    • Projects listings columns applying case when public.can_view_field(...) then value else null end on sensitive fields.
sql
-- Lines 24-33 from supabase/migrations/20250819120000_update_listing_view_secure_complete.sql
  case when public.can_view_field(l.id,'monthly_revenue') then l.monthly_revenue else null end as monthly_revenue,
  case when public.can_view_field(l.id,'monthly_profit') then l.monthly_profit else null end as monthly_profit,
  case when public.can_view_field(l.id,'monthly_traffic') then l.monthly_traffic else null end as monthly_traffic,
  case when public.can_view_field(l.id,'number_of_customers') then l.number_of_customers else null end as number_of_customers,
  • RLS on blurred_fields: public SELECT; INSERT/UPDATE/DELETE admin-only (via is_admin_user()).

Frontend integration

  • Hook: src/hooks/useBlurredFields.ts — determines per field if blurred for current user (admin/owner bypass; level from subscriptions).
  • Components: subscription/BlurredContent, subscription/SmartBlurredContent — show BlurGate/CompactBlurGate when a field is blurred.
  • Selective fetching: useSelectiveDataLoading + FIELD_TO_COLUMN_MAP prevents selecting columns that are blurred.

Mapping field_key → columns (frontend)

typescript
// Lines 3-52 from src/constants/listingFieldMappings.ts
// Map field keys to database column names based on admin blurred fields settings
export const FIELD_TO_COLUMN_MAP: Record<string, string[]> = {
  'price': ['price'],
  'monthly_revenue': ['monthly_revenue'],
  'monthly_profit': ['monthly_profit'], 
  'monthly_traffic': ['monthly_traffic'],
  'number_of_customers': ['number_of_customers'],
  'financial_metrics': ['monthly_revenue', 'monthly_profit'],
  'price_reasoning': ['price_reasoning', 'price_reasoning_ro', 'price_reasoning_en'],
  'reason_for_selling': ['reason_for_selling', 'reason_for_selling_ro', 'reason_for_selling_en'],
  'target_audience': ['target_audience', 'target_audience_ro', 'target_audience_en'],
  'competitors': ['competitors', 'competitors_ro', 'competitors_en'],
  'growth_opportunities': ['growth_opportunities', 'growth_opportunities_ro', 'growth_opportunities_en'],
  'business_model': ['business_model', 'business_model_ro', 'business_model_en'],
  'traffic_metrics_screenshot': ['traffic_metrics_screenshot'],
  'revenue_metrics_screenshot': ['revenue_metrics_screenshot'],
  'screenshot_url': ['screenshot_url'],
  'images': ['images'],
  'established_date': ['established_date'],
  'technologies': ['technologies'],
  'included_assets': ['included_assets'],
  'description': ['description_introduction_startup_description', 'description_introduction_startup_description_ro', 'description_introduction_startup_description_en'],
  'website_url': ['website_url'],
  'traffic_screenshot': ['traffic_metrics_screenshot'],
  'screenshots': ['screenshot_url', 'traffic_metrics_screenshot', 'revenue_metrics_screenshot'],
  // Additional fields from admin blurred fields list
  'address': [],  // No address column in listings table
  'action_contact_seller': [],  // UI action, not data column
  'messaging': [],  // UI action, not data column  
  'action_make_offer': [],  // UI action, not data column
  'seller_information': [],  // This relates to seller profile data
  'traffic_metrics': ['monthly_traffic', 'traffic_metrics_screenshot'],
  
  // Structured description fields - these need to be mapped to ensure they're loaded
  'description_introduction_startup_description': ['description_introduction_startup_description', 'description_introduction_startup_description_ro', 'description_introduction_startup_description_en'],
  'description_introduction_business_model': ['description_introduction_business_model', 'description_introduction_business_model_ro', 'description_introduction_business_model_en'],
  'description_introduction_key_strengths': ['description_introduction_key_strengths', 'description_introduction_key_strengths_ro', 'description_introduction_key_strengths_en'],
  'description_introduction_team_founder': ['description_introduction_team_founder', 'description_introduction_team_founder_ro', 'description_introduction_team_founder_en'],
  'description_content_included_assets': ['description_content_included_assets', 'description_content_included_assets_ro', 'description_content_included_assets_en'],
  'description_content_operations_time': ['description_content_operations_time', 'description_content_operations_time_ro', 'description_content_operations_time_en'],
  'description_content_technology_stack': ['description_content_technology_stack', 'description_content_technology_stack_ro', 'description_content_technology_stack_en'],
  'description_content_financial_performance': ['description_content_financial_performance', 'description_content_financial_performance_ro', 'description_content_financial_performance_en'],
  'description_content_growth_opportunities': ['description_content_growth_opportunities', 'description_content_growth_opportunities_ro', 'description_content_growth_opportunities_en'],
  'description_content_expenses': ['description_content_expenses', 'description_content_expenses_ro', 'description_content_expenses_en'],
  'description_conclusion_selling_reason': ['description_conclusion_selling_reason', 'description_conclusion_selling_reason_ro', 'description_conclusion_selling_reason_en'],
  'description_conclusion_buyer_benefits': ['description_conclusion_buyer_benefits', 'description_conclusion_buyer_benefits_ro', 'description_conclusion_buyer_benefits_en'],
  'description_conclusion_risks_challenges': ['description_conclusion_risks_challenges', 'description_conclusion_risks_challenges_ro', 'description_conclusion_risks_challenges_en'],
  'description_conclusion_ideal_buyer': ['description_conclusion_ideal_buyer', 'description_conclusion_ideal_buyer_ro', 'description_conclusion_ideal_buyer_en'],
  'terms_conditions': ['terms_conditions', 'terms_conditions_ro', 'terms_conditions_en'],
};
typescript
// Lines 68-81 from src/constants/listingFieldMappings.ts
// Fields that should always be fetched, even if blurred, because they are visually blurred on the frontend
export const VISUALLY_BLURRED_FIELDS_TO_FETCH = [
  ...allStructuredDescriptionFieldKeys, // All structured description fields
  'monthly_revenue',
  'monthly_profit',
  'monthly_traffic',
  'number_of_customers',
  'price_reasoning',
  'target_audience',
  'competitors',
  'growth_opportunities',
  'business_model',
  'terms_conditions',
];
typescript
// Lines 83-100 from src/constants/listingFieldMappings.ts
// Fields that should be completely blocked (not fetched) if blurred
export const COMPLETELY_BLOCKED_FIELDS_IF_BLURRED = [
  'screenshot_url',
  'images',
  'revenue_metrics_screenshot',
  'traffic_metrics_screenshot',
  'website_url',
  // Parent fields that control visibility of child fields, but don't have direct columns themselves
  'financial_metrics',
  'traffic_metrics',
  'description',
  'screenshots',
  'address',
  'action_contact_seller',
  'messaging',
  'action_make_offer',
  'seller_information',
];

Administration (UI & RPC)

  • UI: src/components/admin/subscription-plans/BlurredFieldsManager.tsx (list, create, edit, toggle per plan)
  • RPC: admin_toggle_blurred_field(id, plan, enabled) — toggles plan flag for an entry
  • Permissions: only admin can INSERT/UPDATE/DELETE; SELECT is public (dedicated RLS policy)

Actual field matrix (from cloud)

Legend: ✅ = blurred for segment; ❌ = visible.

field_keyUnauthFreeStarterPro
action_add_watchlist
action_contact_seller
action_favorite
action_make_offer
address
business_model
competitors
description_conclusion_buyer_benefits
description_conclusion_ideal_buyer
description_conclusion_risks_challenges
description_conclusion_selling_reason
description_content_expenses
description_content_financial_performance
description_content_growth_opportunities
description_content_included_assets
description_content_operations_time
description_content_technology_stack
description_introduction_business_model
description_introduction_key_strengths
description_introduction_startup_description
description_introduction_team_founder
established_date
explore_filters
explore_search
explore_sort
financial_data_financial_metrics
growth_opportunities
images
messaging
monthly_profit
monthly_revenue
monthly_traffic
number_of_customers
price
price_reasoning
profit
reason_for_selling
resources_technologies_sidebar_revenue_metrics_screenshot
revenue
screenshots
screenshot_url
seller_information
target_audience
technologies
terms_conditions
traffic_audience_traffic_audience_traffic_screenshot
traffic_metrics
traffic_metrics_screenshot
traffic_tab
website_url

Notes:

  • "✅" indicates "blurred for that segment"; access becomes visible only if can_view_field(...) returns true.
  • Listing owner and Admin have full access regardless of the matrix.

Quick SQL (validation)

sql
-- Current configuration
select field_key, is_blurred_for_unauthenticated, is_blurred_for_free, is_blurred_for_starter, is_blurred_for_pro
from public.blurred_fields order by field_key;

-- Visibility test for a given listing
select 'monthly_profit' as key, public.can_view_field('LISTING_UUID','monthly_profit');
  • docs/architecture/access-control.md — RLS principles
  • docs/features/listings.md — listing page integration

API Contract (JSON)

blurred_fields row shape

json
{
  "id": "uuid",
  "field_key": "string",
  "field_name": "string",
  "field_description": "string | null",
  "is_blurred_for_unauthenticated": "boolean",
  "is_blurred_for_free": "boolean",
  "is_blurred_for_starter": "boolean",
  "is_blurred_for_pro": "boolean",
  "created_at": "string (ISO 8601)",
  "updated_at": "string (ISO 8601)"
}

Example

json
{
  "id": "9f9a0c9d-5d4b-4d9c-9a5a-8b0c4c9f2e10",
  "field_key": "monthly_profit",
  "field_name": "Monthly Profit",
  "field_description": "3.2 Monthly Profit",
  "is_blurred_for_unauthenticated": true,
  "is_blurred_for_free": false,
  "is_blurred_for_starter": false,
  "is_blurred_for_pro": false,
  "created_at": "2025-08-19T10:15:00.000Z",
  "updated_at": "2025-08-19T10:15:00.000Z"
}

Admin toggle RPC

sql
select public.admin_toggle_blurred_field(
  p_id => '9f9a0c9d-5d4b-4d9c-9a5a-8b0c4c9f2e10',
  p_plan => 'free',            -- 'unauthenticated' | 'free' | 'starter' | 'pro'
  p_enabled => true
);

Client example (JS)

ts
// Fetch config
const { data: blurredFields } = await supabase
  .from('blurred_fields')
  .select('*')
  .order('field_key');

// Check field visibility (server-enforced in view, this is optional UI logic)
const { data: canSee } = await supabase.rpc('can_view_field', {
  p_listing: listingId,
  p_field_key: 'monthly_profit'
});