Appearance
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()
- Columns:
Function (RPC):
public.can_view_field(p_listing uuid, p_field_key text, p_user uuid default auth.uid())- Admin/owner bypass; if
blurred_fieldshas no entry the field is visible; otherwise access is decided by subscription level.
- Admin/owner bypass; if
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
listingscolumns applyingcase when public.can_view_field(...) then value else null endon sensitive fields.
- Projects
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 (viais_admin_user()).
Frontend integration
- Hook:
src/hooks/useBlurredFields.ts— determines per field if blurred for current user (admin/owner bypass; level fromsubscriptions). - Components:
subscription/BlurredContent,subscription/SmartBlurredContent— show BlurGate/CompactBlurGate when a field is blurred. - Selective fetching:
useSelectiveDataLoading+FIELD_TO_COLUMN_MAPprevents 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_key | Unauth | Free | Starter | Pro |
|---|---|---|---|---|
| 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');Related
docs/architecture/access-control.md— RLS principlesdocs/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'
});