Files
felhom.eu/documentation/audits/security-2026-11-02/bench/sparkyfitness/compose-final.log
T

5360 lines
391 KiB
Plaintext

[2Ksparkyfitness-server | '20251020022200_update_rls_policies.sql',
sparkyfitness-server | '20251020022500_fix_food_select_policy.sql',
sparkyfitness-server | '20251020030000_simplify_food_select_policy.sql',
sparkyfitness-server | '20251020031000_update_food_select_policy_with_sharing.sql',
sparkyfitness-server | '20251020032200_add_all_owner_centric_policies.sql',
sparkyfitness-server | '20251020033400_add_food_variants_rls_policy.sql',
sparkyfitness-server | '20251020033700_fix_food_variants_rls_policy.sql',
sparkyfitness-server | '20251020230839_fix_family_access_rls_policies.sql',
sparkyfitness-server | '20251021025423_restrict_family_access_select_policy.sql',
sparkyfitness-server | '20251021034700_add_public_sharing_to_external_providers.sql',
sparkyfitness-server | '20251022201000_fix_exercise_rls_policy.sql',
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_read_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_medications')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_library_access_with_public(owner_uuid uuid, is_shared bool, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid
sparkyfitness-db | OR is_shared
sparkyfitness-db | OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true OR
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | AND (
sparkyfitness-db | p <> 'can_manage_diary'
sparkyfitness-db | OR current_user_id() = owner_uuid
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.set_app_context(UUID, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.set_app_context(p_user_id UUID, p_authenticated_user_id UUID)
sparkyfitness-db | RETURNS void AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- app.user_id is used by RLS to determine whose data is being accessed
sparkyfitness-db | PERFORM set_config('app.user_id', p_user_id::text, false);
sparkyfitness-db |
sparkyfitness-db | -- app.authenticated_user_id is the actual logged-in user
sparkyfitness-db | PERFORM set_config('app.authenticated_user_id', p_authenticated_user_id::text, false);
sparkyfitness-db | END;
sparkyfitness-db | $$ LANGUAGE plpgsql SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.can_access_user_data(UUID, TEXT, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.can_access_user_data(
sparkyfitness-db | target_user_id UUID,
sparkyfitness-db | permission_type TEXT,
sparkyfitness-db | auth_user_id UUID
sparkyfitness-db | ) RETURNS BOOLEAN AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Self access
sparkyfitness-db | IF target_user_id = auth_user_id THEN
sparkyfitness-db | RETURN TRUE;
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | -- Family access check
sparkyfitness-db | RETURN EXISTS (
sparkyfitness-db | SELECT 1
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | WHERE fa.family_user_id = auth_user_id
sparkyfitness-db | AND fa.owner_user_id = target_user_id
sparkyfitness-db | AND fa.is_active = TRUE
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > NOW())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>permission_type)::BOOLEAN = TRUE
sparkyfitness-db | OR
sparkyfitness-db | -- Mapping for common permission names
sparkyfitness-db | (permission_type = 'diary' AND (fa.access_permissions->>'can_manage_diary')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'checkin' AND (fa.access_permissions->>'can_manage_checkin')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'reports' AND (fa.access_permissions->>'can_view_reports')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-server | '20251022203000_snapshot_exercise_details_in_entries.sql',
sparkyfitness-server | '20251023120500_update_external_providers_select_policy_for_public.sql',
sparkyfitness-server | '20251023221000_update_meals_rls_policy.sql',
sparkyfitness-db | -- Inheritance: reports permission grants read access to others
sparkyfitness-db | (permission_type IN ('calorie', 'diary', 'mood', 'sleep', 'exercise', 'water', 'checkin')
sparkyfitness-db | AND (COALESCE((fa.access_permissions->>'reports')::BOOLEAN, FALSE)
sparkyfitness-db | OR COALESCE((fa.access_permissions->>'can_view_reports')::BOOLEAN, FALSE)))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | -- Helper function to check if authenticated user is admin
sparkyfitness-db | CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean
sparkyfitness-server | '20251023221500_update_meal_plan_template_assignments_rls_policy.sql',
sparkyfitness-server | '20251023221501_add_meal_id_to_food_entries.sql',
sparkyfitness-server | '20251024230330_add_withings_fields_to_external_data_providers.sql',
sparkyfitness-server | '20251025210900_add_source_to_custom_measurements.sql',
sparkyfitness-server | '20251026125900_add_distance_and_hr_to_exercise_entries_activity_details.sql',
sparkyfitness-server | '202510310132_add_language_to_user_preferences.sql',
sparkyfitness-server | '20251101151100_create_sleep_entries_table.sql',
sparkyfitness-server | '20251103000000_add_onboarding_schema.sql',
sparkyfitness-server | '20251112000000_add_display_name_to_custom_categories.sql',
sparkyfitness-server | '20251114205134_create_exercise_preset_entries_table.sql',
sparkyfitness-server | '20251115003147_add_source_to_exercise_preset_entries.sql',
sparkyfitness-server | '20251115003301_add_exercise_preset_entry_id_to_activity_details.sql',
sparkyfitness-server | '20251115004027_alter_exercise_entry_id_nullable_in_activity_details.sql',
sparkyfitness-server | '20251120011100_add_mfa_magic_link_columns.sql',
sparkyfitness-server | '20251121011500_add_email_mfa_columns_to_users.sql',
sparkyfitness-server | '202511220110_add_default_external_data_providers.sql',
sparkyfitness-server | '202511232110_delete_raw_stress_data_categories.sql',
sparkyfitness-server | '202511270109_add_calorie_goal_adjustment_mode_to_user_preferences.sql',
sparkyfitness-server | '20251127194107_create_food_entry_meals_and_link_food_entries.sql',
sparkyfitness-server | '20251127194108_migrate_aggregated_food_entries_to_food_entry_meals.sql',
sparkyfitness-server | '20251128030115_add_tandoor_provider_type.sql',
sparkyfitness-server | '20251205000000_add_energy_unit_to_user_preferences.sql',
sparkyfitness-server | '20251206000000_add_nutrient_calculation_algorithms.sql',
sparkyfitness-server | '20251206000001_add_serving_size_to_meals.sql',
sparkyfitness-server | '20251207000001_add_default_meal_quantity.sql',
sparkyfitness-server | '20251215005329_add_usda_to_provider_type_check.sql',
sparkyfitness-server | '20251221000000_create_fasting_logs.sql',
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public."user" u
sparkyfitness-db | WHERE u.id = authenticated_user_id()
sparkyfitness-db | AND u.role = 'admin'
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-server | '20251221010000_drop_mood_weight_from_fasting_logs.sql',
sparkyfitness-server | '20251226150700_add_mobile_health_data_features.sql',
sparkyfitness-server | '20251228000000_add_custom_nutrients_tables.sql',
sparkyfitness-server | ... 193 more items
sparkyfitness-server | ]
sparkyfitness-server | [2026-11-03T02:00:58.461Z] [INFO] Ensuring permissions for role: "sparkyapp"
sparkyfitness-server | [2026-11-03T02:00:58.511Z] [INFO] Successfully ensured permissions for role: "sparkyapp"
sparkyfitness-server | [2026-11-03T02:00:58.512Z] [INFO] Permissions granted to application user.
sparkyfitness-server | [2026-11-03T02:00:58.512Z] [INFO] Applying all RLS policies from rls_policies.sql...
sparkyfitness-server | [2026-11-03T02:00:58.634Z] [ERROR] Error applying RLS policies: error: relation "public.user_custom_symptom_locations" does not exist
sparkyfitness-server | at /app/SparkyFitnessServer/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/client.js:631:17
sparkyfitness-server | at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
sparkyfitness-server | at async applyRlsPolicies (/app/SparkyFitnessServer/utils/applyRlsPolicies.ts:15:5) {
sparkyfitness-db | -- Step 4: Define generic policy creation functions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY owner_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_shared_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (%I = current_user_id());
sparkyfitness-db | ', table_name, id_column);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_diary_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_checkin_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-server | length: 282,
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_checkin_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''))
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_library_policy(table_name text, shared_column text, permissions text[]) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | DECLARE
sparkyfitness-db | quoted_permissions text;
sparkyfitness-db | shared_expression text;
sparkyfitness-server | severity: 'ERROR',
sparkyfitness-db | BEGIN
sparkyfitness-server | code: '42P01',
sparkyfitness-db | -- Quote each permission name to ensure valid ARRAY syntax
sparkyfitness-server | detail: undefined,
sparkyfitness-db | SELECT array_to_string(ARRAY(
sparkyfitness-db | SELECT quote_literal(p) FROM unnest(permissions) p
sparkyfitness-db | ), ',') INTO quoted_permissions;
sparkyfitness-db |
sparkyfitness-db | -- Use boolean false if shared_column is 'false', otherwise treat as column name
sparkyfitness-db | IF shared_column = 'false' THEN
sparkyfitness-db | shared_expression := 'false';
sparkyfitness-db | ELSE
sparkyfitness-db | shared_expression := quote_ident(shared_column);
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-server | hint: undefined,
sparkyfitness-server | position: undefined,
sparkyfitness-server | internalPosition: undefined,
sparkyfitness-server | internalQuery: undefined,
sparkyfitness-server | where: 'SQL statement "ALTER TABLE public.user_custom_symptom_locations ENABLE ROW LEVEL SECURITY;"\n' +
sparkyfitness-server | 'PL/pgSQL function inline_code_block line 86 at EXECUTE',
sparkyfitness-server | schema: undefined,
sparkyfitness-server | table: undefined,
sparkyfitness-server | column: undefined,
sparkyfitness-server | dataType: undefined,
sparkyfitness-server | constraint: undefined,
sparkyfitness-server | file: 'namespace.c',
sparkyfitness-server | line: '631',
sparkyfitness-server | routine: 'RangeVarGetRelidExtended'
sparkyfitness-server | }
sparkyfitness-server | Failed to start server: error: relation "public.user_custom_symptom_locations" does not exist
sparkyfitness-server | at /app/SparkyFitnessServer/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/client.js:631:17
sparkyfitness-server | at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
sparkyfitness-server | at async applyRlsPolicies (/app/SparkyFitnessServer/utils/applyRlsPolicies.ts:15:5) {
sparkyfitness-server | length: 282,
sparkyfitness-server | severity: 'ERROR',
sparkyfitness-server | code: '42P01',
sparkyfitness-server | detail: undefined,
sparkyfitness-server | hint: undefined,
sparkyfitness-server | position: undefined,
sparkyfitness-server | internalPosition: undefined,
sparkyfitness-server | internalQuery: undefined,
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-server | where: 'SQL statement "ALTER TABLE public.user_custom_symptom_locations ENABLE ROW LEVEL SECURITY;"\n' +
sparkyfitness-db | USING (has_library_access_with_public(user_id, %s, ARRAY[%s]));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | ', table_name, shared_expression, quoted_permissions, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.create_medication_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-server | 'PL/pgSQL function inline_code_block line 86 at EXECUTE',
sparkyfitness-server | schema: undefined,
sparkyfitness-server | table: undefined,
sparkyfitness-server | column: undefined,
sparkyfitness-server | dataType: undefined,
sparkyfitness-server | constraint: undefined,
sparkyfitness-server | file: 'namespace.c',
sparkyfitness-server | line: '631',
sparkyfitness-server | routine: 'RangeVarGetRelidExtended'
sparkyfitness-server | }
sparkyfitness-server | [dotenv@17.3.1] injecting env (0) from ../.env -- tip: 🔐 prevent building .env in docker: https://dotenvx.com/prebuild
sparkyfitness-server | [Secrets] Checking for secret files to load...
sparkyfitness-server | [Secrets] No secrets loaded from files.
sparkyfitness-server | [2026-11-03T02:01:30.167Z] [INFO] Environment variable pre-flight checks passed successfully.
sparkyfitness-server | Starting server...
sparkyfitness-server | [2026-11-03T02:01:38.691Z] [INFO] [withingsService] Withings data source configured to: withings
sparkyfitness-server | [2026-11-03T02:01:38.693Z] [INFO] [fitbitService] Fitbit data source configured to: fitbit
sparkyfitness-server | [2026-11-03T02:01:38.693Z] [INFO] [googleHealthService] Google Health data source: googlehealth
sparkyfitness-server | [2026-11-03T02:01:38.695Z] [INFO] [polarService] Polar data source configured to: polar
sparkyfitness-server | [2026-11-03T02:01:38.696Z] [INFO] [stravaService] Strava data source configured to: strava
sparkyfitness-server | [2026-11-03T02:01:38.696Z] [INFO] [hevyService] Hevy data source configured to: hevy
sparkyfitness-server | [2026-11-03T02:01:39.462Z] [INFO] UserProfileRoutes UPLOADS_DIR: /app/SparkyFitnessServer/uploads/avatars
sparkyfitness-server | DEBUG: SPARKY_FITNESS_FRONTEND_URL is: https://sparky.gate.invalid
sparkyfitness-server | [SECURITY] Private network CORS is ENABLED. Ensure this is only on self-hosted/private networks.
sparkyfitness-server | SparkyFitnessServer UPLOADS_BASE_DIR: /app/SparkyFitnessServer/uploads
sparkyfitness-server | [2026-11-03T02:01:39.634Z] [INFO] Ensured temporary upload directory exists: /app/SparkyFitnessServer/temp_uploads/
sparkyfitness-server | [2026-11-03T02:01:39.657Z] [INFO] Role "sparkyapp" already exists.
sparkyfitness-server | [2026-11-03T02:01:39.659Z] [INFO] Ensured schema_migrations table exists.
sparkyfitness-server | [2026-11-03T02:01:39.664Z] [INFO] Applied migrations: [
sparkyfitness-server | '20250703170640_InitialDB.sql',
sparkyfitness-server | '20250710062500_create_goal_presets_table.sql',
sparkyfitness-server | '20250711173824_meal_features.sql',
sparkyfitness-server | '20250712163617_refactor_data_providers_and_add_exercise_source_id.sql',
sparkyfitness-server | '20250713125323_create_oidc_settings_and_update_users_table.sql',
sparkyfitness-server | '20250713135900_add_role_to_users_table.sql',
sparkyfitness-server | '20250713142300_add_oidc_config_fields.sql',
sparkyfitness-server | '20250714000000_create_session_table.sql',
sparkyfitness-server | '20250714175824_add_api_key_tag_to_ai_service_settings.sql',
sparkyfitness-server | '20250717190700_add_cascading_deletes.sql',
sparkyfitness-server | '20250717233929_add_is_quick_food_to_foods.sql',
sparkyfitness-server | '20250718190226_add_food_display_limit_to_user_preferences.sql',
sparkyfitness-server | '20250719131500_add_meal_percentages.sql',
sparkyfitness-server | '20250719215400_add_login_management_to_oidc_settings.sql',
sparkyfitness-server | '20250720010000_create_user_nutrient_display_preferences.sql',
sparkyfitness-server | '20250720201800_rename_food_display_limit_to_item_display_limit.sql',
sparkyfitness-server | '20250720220000_add_water_tracking_features.sql',
sparkyfitness-server | '20250720233836_alter_water_goal_ml_to_numeric.sql',
sparkyfitness-server | '20250721000000_add_servings_per_container_to_water_containers.sql',
sparkyfitness-server | '20250721000000_alter_water_container_volume_to_numeric.sql',
sparkyfitness-server | '20250721014900_alter_water_ml_to_numeric.sql',
sparkyfitness-server | '20250819062400_add_mealie_provider_type.sql',
sparkyfitness-server | '20250819083500_add_base_url_to_external_data_providers.sql',
sparkyfitness-server | '202508201215_add_garmin_integration_schema.sql',
sparkyfitness-server | '20250821090000_update_external_data_providers_provider_type_check.sql',
sparkyfitness-server | '20250821091500_add_garth_token_dump_to_external_data_providers.sql',
sparkyfitness-server | '20250821102000_remove_redundant_token_fields_from_external_data_providers.sql',
sparkyfitness-server | '20250831063900_create_mood_entries_table.sql',
sparkyfitness-server | '20250927180257_alter_exercises_table.sql',
sparkyfitness-server | '20250927212000_add_free_exercise_db_to_provider_type_check.sql',
sparkyfitness-server | '20250930002100_add_sets_reps_weight_to_exercise_entries.sql',
sparkyfitness-server | '20251001232600_add_gender_to_users_table.sql',
sparkyfitness-server | '20251002182306_create_backup_settings_table.sql',
sparkyfitness-server | '20251002224500_add_password_reset_to_users_table.sql',
sparkyfitness-server | '20251006021609_add_glycemic_index_to_foods_and_food_variants.sql',
sparkyfitness-server | '20251006220500_create_admin_activity_logs_table.sql',
sparkyfitness-server | '20251008224256_add_workout_features.sql',
sparkyfitness-server | '20251012225400_add_primary_keys_to_workout_tables.sql',
sparkyfitness-server | '20251012225500_add_multiple_sets_to_presets.sql',
sparkyfitness-server | '20251012225900_add_multiple_sets_to_plans.sql',
sparkyfitness-server | '20251012230000_add_multiple_sets_to_exercise_entries.sql',
sparkyfitness-server | '20251015223200_convert_weights_to_kg_and_set_default.sql',
sparkyfitness-server | '202510170132_add_text_measurements.sql',
sparkyfitness-server | '20251017200900_add_multiple_oidc_providers.sql',
sparkyfitness-server | '20251017204200_create_global_settings.sql',
sparkyfitness-server | '20251018001151_add_bmr_and_bodyfat_preferences.sql',
sparkyfitness-server | '20251018223700_add_signing_alg_to_oidc_providers.sql',
sparkyfitness-server | '20251019013200_merged_schema_and_data.sql',
sparkyfitness-server | '20251019013300_merged_rls_policies.sql',
sparkyfitness-server | '20251019154515_update_rls_policies_for_shared_items.sql',
sparkyfitness-server | '20251019203500_fix_rls_permissions_for_public_foods.sql',
sparkyfitness-server | '20251019204000_correct_variant_rls_policy.sql',
sparkyfitness-server | '20251019212500_update_measurement_rls_policies.sql',
sparkyfitness-server | '20251019222100_add_audit_columns_to_measurements.sql',
sparkyfitness-server | '20251020001000_fix_family_access_insert_policy.sql',
sparkyfitness-server | '20251020022000_set_default_shared_with_public.sql',
sparkyfitness-server | '20251020022200_update_rls_policies.sql',
sparkyfitness-server | '20251020022500_fix_food_select_policy.sql',
sparkyfitness-server | '20251020030000_simplify_food_select_policy.sql',
sparkyfitness-server | '20251020031000_update_food_select_policy_with_sharing.sql',
sparkyfitness-server | '20251020032200_add_all_owner_centric_policies.sql',
sparkyfitness-server | '20251020033400_add_food_variants_rls_policy.sql',
sparkyfitness-server | '20251020033700_fix_food_variants_rls_policy.sql',
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_medication_read_access(user_id));
sparkyfitness-server | '20251020230839_fix_family_access_rls_policies.sql',
sparkyfitness-server | '20251021025423_restrict_family_access_select_policy.sql',
sparkyfitness-server | '20251021034700_add_public_sharing_to_external_providers.sql',
sparkyfitness-server | '20251022201000_fix_exercise_rls_policy.sql',
sparkyfitness-server | '20251022203000_snapshot_exercise_details_in_entries.sql',
sparkyfitness-server | '20251023120500_update_external_providers_select_policy_for_public.sql',
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_medication_access(user_id))
sparkyfitness-db | WITH CHECK (has_medication_access(user_id));
sparkyfitness-server | '20251023221000_update_meals_rls_policy.sql',
sparkyfitness-server | '20251023221500_update_meal_plan_template_assignments_rls_policy.sql',
sparkyfitness-server | '20251023221501_add_meal_id_to_food_entries.sql',
sparkyfitness-server | '20251024230330_add_withings_fields_to_external_data_providers.sql',
sparkyfitness-server | '20251025210900_add_source_to_custom_measurements.sql',
sparkyfitness-server | '20251026125900_add_distance_and_hr_to_exercise_entries_activity_details.sql',
sparkyfitness-server | '202510310132_add_language_to_user_preferences.sql',
sparkyfitness-server | '20251101151100_create_sleep_entries_table.sql',
sparkyfitness-server | '20251103000000_add_onboarding_schema.sql',
sparkyfitness-server | '20251112000000_add_display_name_to_custom_categories.sql',
sparkyfitness-server | '20251114205134_create_exercise_preset_entries_table.sql',
sparkyfitness-server | '20251115003147_add_source_to_exercise_preset_entries.sql',
sparkyfitness-server | '20251115003301_add_exercise_preset_entry_id_to_activity_details.sql',
sparkyfitness-server | '20251115004027_alter_exercise_entry_id_nullable_in_activity_details.sql',
sparkyfitness-server | '20251120011100_add_mfa_magic_link_columns.sql',
sparkyfitness-server | '20251121011500_add_email_mfa_columns_to_users.sql',
sparkyfitness-server | '202511220110_add_default_external_data_providers.sql',
sparkyfitness-server | '202511232110_delete_raw_stress_data_categories.sql',
sparkyfitness-server | '202511270109_add_calorie_goal_adjustment_mode_to_user_preferences.sql',
sparkyfitness-server | '20251127194107_create_food_entry_meals_and_link_food_entries.sql',
sparkyfitness-server | '20251127194108_migrate_aggregated_food_entries_to_food_entry_meals.sql',
sparkyfitness-server | '20251128030115_add_tandoor_provider_type.sql',
sparkyfitness-server | '20251205000000_add_energy_unit_to_user_preferences.sql',
sparkyfitness-server | '20251206000000_add_nutrient_calculation_algorithms.sql',
sparkyfitness-server | '20251206000001_add_serving_size_to_meals.sql',
sparkyfitness-server | '20251207000001_add_default_meal_quantity.sql',
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-server | '20251215005329_add_usda_to_provider_type_check.sql',
sparkyfitness-db | -- Step 5: Apply policies to all tables.
sparkyfitness-server | '20251221000000_create_fasting_logs.sql',
sparkyfitness-server | '20251221010000_drop_mood_weight_from_fasting_logs.sql',
sparkyfitness-server | '20251226150700_add_mobile_health_data_features.sql',
sparkyfitness-server | '20251228000000_add_custom_nutrients_tables.sql',
sparkyfitness-server | ... 193 more items
sparkyfitness-server | ]
sparkyfitness-server | [2026-11-03T02:01:39.681Z] [INFO] Ensuring permissions for role: "sparkyapp"
sparkyfitness-server | [2026-11-03T02:01:39.725Z] [INFO] Successfully ensured permissions for role: "sparkyapp"
sparkyfitness-server | [2026-11-03T02:01:39.726Z] [INFO] Permissions granted to application user.
sparkyfitness-server | [2026-11-03T02:01:39.726Z] [INFO] Applying all RLS policies from rls_policies.sql...
sparkyfitness-server | [2026-11-03T02:01:39.845Z] [ERROR] Error applying RLS policies: error: relation "public.user_custom_symptom_locations" does not exist
sparkyfitness-server | at /app/SparkyFitnessServer/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/client.js:631:17
sparkyfitness-server | at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
sparkyfitness-db | -- Custom policy for ai_service_settings to support admin-global + user-owned settings
sparkyfitness-server | at async applyRlsPolicies (/app/SparkyFitnessServer/utils/applyRlsPolicies.ts:15:5) {
sparkyfitness-db | -- Drop ALL possible old policy names before recreating
sparkyfitness-server | length: 282,
sparkyfitness-db | DROP POLICY IF EXISTS owner_policy ON public.ai_service_settings;
sparkyfitness-server | severity: 'ERROR',
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.ai_service_settings;
sparkyfitness-server | code: '42P01',
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.ai_service_settings;
sparkyfitness-server | detail: undefined,
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_select_policy ON public.ai_service_settings;
sparkyfitness-server | hint: undefined,
sparkyfitness-server | position: undefined,
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_insert_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_update_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_delete_policy ON public.ai_service_settings;
sparkyfitness-db | -- SELECT policy: All authenticated users can read public settings, users can read their own
sparkyfitness-db | CREATE POLICY ai_service_settings_select_policy ON public.ai_service_settings FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = TRUE AND authenticated_user_id() IS NOT NULL) OR
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id())
sparkyfitness-db | );
sparkyfitness-server | internalPosition: undefined,
sparkyfitness-server | internalQuery: undefined,
sparkyfitness-server | where: 'SQL statement "ALTER TABLE public.user_custom_symptom_locations ENABLE ROW LEVEL SECURITY;"\n' +
sparkyfitness-server | 'PL/pgSQL function inline_code_block line 86 at EXECUTE',
sparkyfitness-server | schema: undefined,
sparkyfitness-server | table: undefined,
sparkyfitness-server | column: undefined,
sparkyfitness-server | dataType: undefined,
sparkyfitness-server | constraint: undefined,
sparkyfitness-server | file: 'namespace.c',
sparkyfitness-server | line: '631',
sparkyfitness-server | routine: 'RangeVarGetRelidExtended'
sparkyfitness-server | }
sparkyfitness-server | Failed to start server: error: relation "public.user_custom_symptom_locations" does not exist
sparkyfitness-server | at /app/SparkyFitnessServer/node_modules/.pnpm/pg@8.20.0/node_modules/pg/lib/client.js:631:17
sparkyfitness-server | at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
sparkyfitness-server | at async applyRlsPolicies (/app/SparkyFitnessServer/utils/applyRlsPolicies.ts:15:5) {
sparkyfitness-server | length: 282,
sparkyfitness-server | severity: 'ERROR',
sparkyfitness-server | code: '42P01',
sparkyfitness-server | detail: undefined,
sparkyfitness-server | hint: undefined,
sparkyfitness-server | position: undefined,
sparkyfitness-server | internalPosition: undefined,
sparkyfitness-server | internalQuery: undefined,
sparkyfitness-server | where: 'SQL statement "ALTER TABLE public.user_custom_symptom_locations ENABLE ROW LEVEL SECURITY;"\n' +
sparkyfitness-server | 'PL/pgSQL function inline_code_block line 86 at EXECUTE',
sparkyfitness-server | schema: undefined,
sparkyfitness-server | table: undefined,
sparkyfitness-server | column: undefined,
sparkyfitness-server | dataType: undefined,
sparkyfitness-db | -- INSERT policy: Users can create their own settings, admins can create public settings
sparkyfitness-server | constraint: undefined,
sparkyfitness-db | CREATE POLICY ai_service_settings_insert_policy ON public.ai_service_settings FOR INSERT TO PUBLIC
sparkyfitness-server | file: 'namespace.c',
sparkyfitness-db | WITH CHECK (
sparkyfitness-server | line: '631',
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- UPDATE policy: Users can update their own settings, admins can update public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_update_policy ON public.ai_service_settings FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- DELETE policy: Users can delete their own settings, admins can delete public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_delete_policy ON public.ai_service_settings FOR DELETE TO PUBLIC
sparkyfitness-server | routine: 'RangeVarGetRelidExtended'
sparkyfitness-server | }
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Owner-only access tables
sparkyfitness-db | -- Tier 1: Strictly Private (no delegation allowed)
sparkyfitness-db | SELECT create_owner_policy('api_key', 'reference_id');
sparkyfitness-db | SELECT create_owner_policy('user_oidc_links');
sparkyfitness-db | SELECT create_owner_policy('sparky_chat_history');
sparkyfitness-db |
sparkyfitness-db | -- Profiles: delegates can read (with any meaningful permission) but only owner can write.
sparkyfitness-db | -- Delegates do not need to modify another user's profile to manage their diary.
sparkyfitness-db | CREATE POLICY select_policy ON public.profiles FOR SELECT TO PUBLIC USING (has_profile_read_access(id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.profiles FOR ALL TO PUBLIC USING (authenticated_user_id() = id) WITH CHECK (authenticated_user_id() = id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.user_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('user_goals');
sparkyfitness-db | SELECT create_diary_policy('weekly_goal_plans');
sparkyfitness-db | SELECT create_diary_policy('user_water_containers');
sparkyfitness-db | SELECT create_diary_policy('user_custom_nutrients');
sparkyfitness-db | SELECT create_diary_policy('user_allergen_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Nutrient display preferences: delegates can read but only owner can rearrange their own columns.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_nutrient_display_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_nutrient_display_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Dashboard layouts: delegates can read but only owner can rearrange their own dashboard.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_dashboard_layouts FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_dashboard_layouts FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | SELECT create_diary_policy('goal_presets');
sparkyfitness-db | SELECT create_diary_policy('meal_plans');
sparkyfitness-db | SELECT create_checkin_policy('mood_entries');
sparkyfitness-db |
sparkyfitness-db | -- Admin Activity Logs: Only the admin who performed the action or other admins can view
sparkyfitness-db | CREATE POLICY admin_only_select ON public.admin_activity_logs FOR SELECT TO PUBLIC
sparkyfitness-db | USING (admin_user_id = current_user_id() OR is_admin());
sparkyfitness-db | CREATE POLICY admin_only_insert ON public.admin_activity_logs FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (admin_user_id = current_user_id() AND is_admin());
sparkyfitness-db |
sparkyfitness-db | -- Diary access tables
sparkyfitness-db | SELECT create_checkin_policy('check_in_measurements');
sparkyfitness-db | SELECT create_checkin_policy('check_in_photos');
sparkyfitness-db | -- Custom categories/measurements are surfaced through the check-in feature and
sparkyfitness-db | -- every /measurements route is guarded by checkPermissionMiddleware('checkin'),
sparkyfitness-db | -- so their RLS must use the check-in policy. The previous diary policy required
sparkyfitness-db | -- can_manage_diary to write, which blocked check-in delegates (e.g. the GLP-1
sparkyfitness-db | -- daily check-in) from saving even though the route allowed them.
sparkyfitness-db | SELECT create_checkin_policy('custom_categories');
sparkyfitness-db | SELECT create_checkin_policy('custom_measurements');
sparkyfitness-db | SELECT create_diary_policy('exercise_entries');
sparkyfitness-db | -- Custom policy for exercise_entries to allow access if linked to an owned exercise_preset_entry
sparkyfitness-db | CREATE POLICY select_exercise_preset_entry_linked_policy ON public.exercise_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | exercise_preset_entry_id IS NOT NULL AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.exercise_preset_entries epe
sparkyfitness-db | WHERE epe.id = exercise_entries.exercise_preset_entry_id AND has_diary_read_access(epe.user_id)
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- The modify policy for exercise_entries is already handled by create_diary_policy('exercise_entries')
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('exercise_preset_entries');
sparkyfitness-db | SELECT create_diary_policy('food_entry_meals');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entries');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entry_stages');
sparkyfitness-db | SELECT create_diary_policy('water_intake');
sparkyfitness-db | SELECT create_diary_policy('water_intake_entries');
sparkyfitness-db |
sparkyfitness-db | -- Library access tables
sparkyfitness-db | SELECT create_library_policy('exercises', 'shared_with_public', ARRAY['can_view_exercise_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('foods', 'shared_with_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meals', 'is_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meal_plan_templates', 'false', ARRAY['can_view_food_library']);
sparkyfitness-db | SELECT create_library_policy('workout_plan_templates', 'false', ARRAY['can_view_exercise_library']);
sparkyfitness-db | SELECT create_library_policy('workout_presets', 'is_public', ARRAY['can_view_exercise_library','can_manage_diary']);
sparkyfitness-db |
sparkyfitness-db | -- Medication & GLP-1 tracker (see migration 20260624000000_add_medication_glp1_schema.sql).
sparkyfitness-db | -- These tables are managed by create_medication_policy at the bottom of this file (Tier 3).
sparkyfitness-db | -- Do NOT apply create_library_policy or create_diary_policy to medication tables.
sparkyfitness-db | SELECT create_owner_policy('user_medication_display_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Cycle & Pregnancy hub (see migration 20260702180000_add_cycle_tracking_schema.sql).
sparkyfitness-db | -- Tier 1 — owner-only. Deliberately stricter than medications: this reproductive
sparkyfitness-db | -- health data is NEVER shared or delegated in v1 (no family/caregiver access).
sparkyfitness-db | SELECT create_owner_policy('cycle_settings');
sparkyfitness-db | SELECT create_owner_policy('cycle_daily_entries');
sparkyfitness-db | SELECT create_owner_policy('cycles');
sparkyfitness-db | SELECT create_owner_policy('user_cycle_display_preferences');
sparkyfitness-db | SELECT create_owner_policy('cycle_test_entries');
sparkyfitness-db |
sparkyfitness-db | -- Pregnancy mode (see migration 20260702200000_add_pregnancy_schema.sql). Tier 1
sparkyfitness-db | -- owner-only. health_appointments is generic but still owner-only in v1.
sparkyfitness-db | SELECT create_owner_policy('pregnancies');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_kick_sessions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_contractions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_photos');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_checklist_state');
sparkyfitness-db | SELECT create_owner_policy('health_appointments');
sparkyfitness-db |
sparkyfitness-db | -- User-defined mood tags. Mood is check-in data (mood_entries uses the check-in
sparkyfitness-db | -- policy), and custom check-in definitions like custom_categories are shared with
sparkyfitness-db | -- check-in delegates — so custom moods follow the same check-in policy for
sparkyfitness-db | -- consistency (a delegate managing the owner's check-in sees the owner's moods).
sparkyfitness-db | SELECT create_checkin_policy('user_custom_moods');
sparkyfitness-db |
sparkyfitness-db | -- Mood display preferences: personal picker config, owner-only.
sparkyfitness-db | SELECT create_owner_policy('user_mood_display_preferences');
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | -- Custom policies for special cases
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_read_access(ee.user_id)));
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)));
sparkyfitness-db |
sparkyfitness-db | -- Provider configs: admin-global (is_public) OR own OR family delegation
sparkyfitness-db | -- Drop any old policy names first (idempotent)
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS insert_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS update_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS delete_policy ON public.external_data_providers;
sparkyfitness-db |
sparkyfitness-db | -- SELECT: admin-global (authenticated users), own, or explicit family delegation
sparkyfitness-db | CREATE POLICY select_policy ON public.external_data_providers FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | -- Admin-created global providers: all authenticated users can see them if active
sparkyfitness-db | (is_public = TRUE AND is_active = TRUE AND authenticated_user_id() IS NOT NULL)
sparkyfitness-db | -- Owner always sees their own providers
sparkyfitness-db | OR (is_public = FALSE AND current_user_id() = user_id)
sparkyfitness-db | OR (
sparkyfitness-db | -- Explicit family delegation: share_external_providers permission, non-strictly-private only, must be active
sparkyfitness-db | is_public = FALSE AND is_active = TRUE AND has_family_access(user_id, 'share_external_providers') AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.external_provider_types ept
sparkyfitness-db | WHERE ept.id = external_data_providers.provider_type
sparkyfitness-db | AND ept.is_strictly_private = FALSE
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- INSERT: users create their own (is_public=FALSE), admins create global (is_public=TRUE)
sparkyfitness-db | CREATE POLICY insert_policy ON public.external_data_providers FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- UPDATE: users update own, admins update global
sparkyfitness-db | CREATE POLICY update_policy ON public.external_data_providers FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- DELETE: users delete own, admins delete global
sparkyfitness-db | CREATE POLICY delete_policy ON public.external_data_providers FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.family_access FOR SELECT TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id OR authenticated_user_id() = family_user_id);
sparkyfitness-db | CREATE POLICY insert_policy ON public.family_access FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db | CREATE POLICY modify_policy ON public.family_access FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY insert_policy ON public.food_entries FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | has_diary_access(user_id) AND (
sparkyfitness-db | (food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = food_entries.food_id)) OR
sparkyfitness-db | (meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = food_entries.meal_id))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY update_policy ON public.food_entries FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | CREATE POLICY delete_policy ON public.food_entries FOR DELETE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_variants FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND has_library_access_with_public(f.user_id, f.shared_with_public, ARRAY['can_view_food_library', 'can_manage_diary'])
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- Food variants are library data: only the owner of the parent food may write
sparkyfitness-db | -- them. Delegates (even can_manage_diary) get read-only access via select_policy
sparkyfitness-db | -- so they can pick serving sizes while logging, but cannot mutate the library.
sparkyfitness-db | CREATE POLICY modify_policy ON public.food_variants FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- meal_foods is polymorphic: a row references either a food (item_type='food')
sparkyfitness-db | -- or another meal (item_type='meal', a reusable sub-meal). Read access follows
sparkyfitness-db | -- the parent meal. Write access requires the caller owns the parent meal AND the
sparkyfitness-db | -- referenced ingredient is accessible to them: a food they can view, or a child
sparkyfitness-db | -- meal they have library access to (prevents linking a meal you cannot see).
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_foods FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND has_library_access_with_public(m.user_id, m.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_foods FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | AND (
sparkyfitness-db | (meal_foods.food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_foods.food_id))
sparkyfitness-db | OR
sparkyfitness-db | (meal_foods.child_meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals cm WHERE cm.id = meal_foods.child_meal_id AND has_library_access_with_public(cm.user_id, cm.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.meal_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_assignment_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercise_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_preset_exercises wpe WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercise_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercises FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_presets wp WHERE wp.id = workout_preset_exercises.workout_preset_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercises FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | -- Strictly Private (Tier 1)
sparkyfitness-db | SELECT create_owner_policy('user_ignored_updates');
sparkyfitness-db |
sparkyfitness-db | -- Meal types: access if user_id is null (system) or if user has diary access
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_types FOR SELECT TO PUBLIC
sparkyfitness-db | USING (user_id IS NULL OR has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_types FOR ALL TO PUBLIC
sparkyfitness-db | USING (user_id = authenticated_user_id())
sparkyfitness-db | WITH CHECK (user_id = authenticated_user_id());
sparkyfitness-db |
sparkyfitness-db | -- Activity details: access if linked exercise entry or preset entry is accessible
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_activity_details FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_read_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_read_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_activity_details FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Shared View-Only (Tier 2)
sparkyfitness-db | SELECT create_checkin_policy('fasting_logs');
sparkyfitness-db | SELECT create_diary_policy('user_meal_visibilities');
sparkyfitness-db | SELECT create_checkin_policy('sleep_need_calculations');
sparkyfitness-db | SELECT create_checkin_policy('daily_sleep_need');
sparkyfitness-db | -- Day classification is a sleep/wellness (check-in) feature, used only by the
sparkyfitness-db | -- sleep-science service. It must use the check-in policy so check-in delegates
sparkyfitness-db | -- can manage it; the previous diary policy was a misclassification.
sparkyfitness-db | SELECT create_checkin_policy('day_classification_cache');
sparkyfitness-db | -- Onboarding data/status: delegates can read (to avoid repeated onboarding prompts when viewing
sparkyfitness-db | -- another user's diary) but only the account owner can submit or reset their own onboarding.
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_data FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_data FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_status FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_status FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Medications & Symptoms (Tier 3 - Delegate Writable with medications permission)
sparkyfitness-db | SELECT create_medication_policy('medications');
sparkyfitness-db | SELECT create_medication_policy('medication_schedules');
sparkyfitness-db | SELECT create_medication_policy('medication_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_pens');
sparkyfitness-db | SELECT create_medication_policy('injection_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_titration_steps');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptoms');
sparkyfitness-db | SELECT create_medication_policy('symptom_entries');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptom_locations');
sparkyfitness-db |
sparkyfitness-db | -- Medications Display Preferences (Tier 2 - Owner-Only Write, Delegate Read)
sparkyfitness-db | CREATE POLICY select_policy ON public.user_medication_display_preferences FOR SELECT TO PUBLIC USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_medication_display_preferences FOR ALL TO PUBLIC USING (authenticated_user_id() = user_id) WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | 2026-11-03 02:58:58.177 CET [704] ERROR: relation "public.user_custom_symptom_locations" does not exist
sparkyfitness-db | 2026-11-03 02:58:58.177 CET [704] CONTEXT: SQL statement "ALTER TABLE public.user_custom_symptom_locations ENABLE ROW LEVEL SECURITY;"
sparkyfitness-db | PL/pgSQL function inline_code_block line 86 at EXECUTE
sparkyfitness-db | 2026-11-03 02:58:58.177 CET [704] STATEMENT: -- File: rls_policies.sql
sparkyfitness-db | -- =============================================================================
sparkyfitness-db | -- THIS FILE IS THE SINGLE SOURCE OF TRUTH FOR ALL RLS POLICIES IN THE APPLICATION.
sparkyfitness-db | -- It is executed on every server startup after migrations to ensure a consistent security state.
sparkyfitness-db | -- This script is generated from the db_schema_backup.sql to ensure all custom policies are included.
sparkyfitness-db | -- =============================================================================
sparkyfitness-db |
sparkyfitness-db | -- Step 1: Purge all existing RLS policies from the public schema in a single operation.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | pol RECORD;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR pol IN SELECT * FROM pg_policies WHERE schemaname = 'public'
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'DROP POLICY IF EXISTS ' || quote_ident(pol.policyname) || ' ON ' || quote_ident(pol.schemaname) || '.' || quote_ident(pol.tablename);
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 2: Enable RLS on all relevant tables to ensure consistent security state.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | table_name text;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR table_name IN SELECT unnest(ARRAY[
sparkyfitness-db | 'ai_service_settings',
sparkyfitness-db | 'check_in_measurements',
sparkyfitness-db | 'check_in_photos',
sparkyfitness-db | 'custom_categories',
sparkyfitness-db | 'custom_measurements',
sparkyfitness-db | 'exercise_entries',
sparkyfitness-db | 'exercise_entry_sets',
sparkyfitness-db | 'exercise_entry_activity_details',
sparkyfitness-db | 'exercises',
sparkyfitness-db | 'exercise_preset_entries',
sparkyfitness-db | 'external_data_providers',
sparkyfitness-db | 'family_access',
sparkyfitness-db | 'food_entries',
sparkyfitness-db | 'food_entry_meals',
sparkyfitness-db | 'food_variants',
sparkyfitness-db | 'foods',
sparkyfitness-db | 'goal_presets',
sparkyfitness-db | 'meal_foods',
sparkyfitness-db | 'meal_plan_template_assignments',
sparkyfitness-db | 'meal_plan_templates',
sparkyfitness-db | 'meal_plans',
sparkyfitness-db | 'meals',
sparkyfitness-db | 'meal_types',
sparkyfitness-db | 'mood_entries',
sparkyfitness-db | 'onboarding_data',
sparkyfitness-db | 'onboarding_status',
sparkyfitness-db | 'profiles',
sparkyfitness-db | 'sparky_chat_history',
sparkyfitness-db | 'admin_activity_logs',
sparkyfitness-db | 'api_key',
sparkyfitness-db | 'user_goals',
sparkyfitness-db | 'user_ignored_updates',
sparkyfitness-db | 'user_meal_visibilities',
sparkyfitness-db | 'user_nutrient_display_preferences',
sparkyfitness-db | 'user_oidc_links',
sparkyfitness-db | 'user_preferences',
sparkyfitness-db | 'user_water_containers',
sparkyfitness-db | 'water_intake',
sparkyfitness-db | 'water_intake_entries',
sparkyfitness-db | 'weekly_goal_plans',
sparkyfitness-db | 'workout_plan_assignment_sets',
sparkyfitness-db | 'workout_plan_template_assignments',
sparkyfitness-db | 'workout_plan_templates',
sparkyfitness-db | 'workout_preset_exercise_sets',
sparkyfitness-db | 'workout_preset_exercises',
sparkyfitness-db | 'workout_presets',
sparkyfitness-db | 'sleep_entries',
sparkyfitness-db | 'sleep_entry_stages',
sparkyfitness-db | 'fasting_logs',
sparkyfitness-db | 'user_custom_nutrients',
sparkyfitness-db | 'user_allergen_preferences',
sparkyfitness-db | 'user_dashboard_layouts',
sparkyfitness-db | 'sleep_need_calculations',
sparkyfitness-db | 'daily_sleep_need',
sparkyfitness-db | 'day_classification_cache',
sparkyfitness-db | 'medications',
sparkyfitness-db | 'medication_schedules',
sparkyfitness-db | 'medication_entries',
sparkyfitness-db | 'medication_pens',
sparkyfitness-db | 'injection_entries',
sparkyfitness-db | 'medication_titration_steps',
sparkyfitness-db | 'user_custom_symptoms',
sparkyfitness-db | 'symptom_entries',
sparkyfitness-db | 'user_medication_display_preferences',
sparkyfitness-db | 'user_custom_symptom_locations',
sparkyfitness-db | 'cycle_settings',
sparkyfitness-db | 'cycle_daily_entries',
sparkyfitness-db | 'cycles',
sparkyfitness-db | 'user_cycle_display_preferences',
sparkyfitness-db | 'cycle_test_entries',
sparkyfitness-db | 'pregnancies',
sparkyfitness-db | 'pregnancy_kick_sessions',
sparkyfitness-db | 'pregnancy_contractions',
sparkyfitness-db | 'pregnancy_photos',
sparkyfitness-db | 'pregnancy_checklist_state',
sparkyfitness-db | 'health_appointments',
sparkyfitness-db | 'user_custom_moods',
sparkyfitness-db | 'user_mood_display_preferences'
sparkyfitness-db | ]::text[])
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'ALTER TABLE public.' || quote_ident(table_name) || ' ENABLE ROW LEVEL SECURITY;';
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 3: Define reusable helper functions for common RLS conditions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION current_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION authenticated_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.authenticated_user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.get_accessible_users(p_user_id UUID)
sparkyfitness-db | RETURNS TABLE(
sparkyfitness-db | user_id UUID,
sparkyfitness-db | full_name TEXT,
sparkyfitness-db | email TEXT,
sparkyfitness-db | permissions JSONB,
sparkyfitness-db | access_end_date TIMESTAMP WITH TIME ZONE
sparkyfitness-db | ) AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | RETURN QUERY
sparkyfitness-db | SELECT
sparkyfitness-db | fa.owner_user_id,
sparkyfitness-db | p.full_name,
sparkyfitness-db | u.email::TEXT,
sparkyfitness-db | fa.access_permissions,
sparkyfitness-db | fa.access_end_date
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | JOIN public.profiles p ON p.id = fa.owner_user_id
sparkyfitness-db | JOIN public."user" u ON u.id = fa.owner_user_id
sparkyfitness-db | WHERE fa.family_user_id = p_user_id
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions);
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.find_user_by_email(p_email TEXT)
sparkyfitness-db | RETURNS UUID AS $func$
sparkyfitness-db | DECLARE
sparkyfitness-db | v_user_id UUID;
sparkyfitness-db | BEGIN
sparkyfitness-db | SELECT id INTO v_user_id
sparkyfitness-db | FROM public."user"
sparkyfitness-db | WHERE LOWER(email) = LOWER(p_email)
sparkyfitness-db | LIMIT 1;
sparkyfitness-db |
sparkyfitness-db | RETURN v_user_id;
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access(owner_uuid uuid, perm text) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions ->> perm)::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access_or(owner_uuid uuid, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_diary')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Centralized helper: returns true if the given permissions JSONB contains
sparkyfitness-db | -- at least one meaningful delegation permission. Used by has_profile_read_access
sparkyfitness-db | -- and get_accessible_users to avoid duplicating the permission key list.
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_any_meaningful_permission(perms jsonb) RETURNS bool
sparkyfitness-db | LANGUAGE sql IMMUTABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT (
sparkyfitness-db | (perms->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (perms->>'can_view_reports')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_profile_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | -- Owner always has access. Family delegates require at least one meaningful permission
sparkyfitness-db | -- (diary, checkin, medications, or reports) to read profile/layout/onboarding data.
sparkyfitness-db | -- A bare family_access row with no permissions does not grant read access.
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions)
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_checkin_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_read_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_medications')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_library_access_with_public(owner_uuid uuid, is_shared bool, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid
sparkyfitness-db | OR is_shared
sparkyfitness-db | OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true OR
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | AND (
sparkyfitness-db | p <> 'can_manage_diary'
sparkyfitness-db | OR current_user_id() = owner_uuid
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.set_app_context(UUID, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.set_app_context(p_user_id UUID, p_authenticated_user_id UUID)
sparkyfitness-db | RETURNS void AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- app.user_id is used by RLS to determine whose data is being accessed
sparkyfitness-db | PERFORM set_config('app.user_id', p_user_id::text, false);
sparkyfitness-db |
sparkyfitness-db | -- app.authenticated_user_id is the actual logged-in user
sparkyfitness-db | PERFORM set_config('app.authenticated_user_id', p_authenticated_user_id::text, false);
sparkyfitness-db | END;
sparkyfitness-db | $$ LANGUAGE plpgsql SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.can_access_user_data(UUID, TEXT, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.can_access_user_data(
sparkyfitness-db | target_user_id UUID,
sparkyfitness-db | permission_type TEXT,
sparkyfitness-db | auth_user_id UUID
sparkyfitness-db | ) RETURNS BOOLEAN AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Self access
sparkyfitness-db | IF target_user_id = auth_user_id THEN
sparkyfitness-db | RETURN TRUE;
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | -- Family access check
sparkyfitness-db | RETURN EXISTS (
sparkyfitness-db | SELECT 1
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | WHERE fa.family_user_id = auth_user_id
sparkyfitness-db | AND fa.owner_user_id = target_user_id
sparkyfitness-db | AND fa.is_active = TRUE
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > NOW())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>permission_type)::BOOLEAN = TRUE
sparkyfitness-db | OR
sparkyfitness-db | -- Mapping for common permission names
sparkyfitness-db | (permission_type = 'diary' AND (fa.access_permissions->>'can_manage_diary')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'checkin' AND (fa.access_permissions->>'can_manage_checkin')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'reports' AND (fa.access_permissions->>'can_view_reports')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | -- Inheritance: reports permission grants read access to others
sparkyfitness-db | (permission_type IN ('calorie', 'diary', 'mood', 'sleep', 'exercise', 'water', 'checkin')
sparkyfitness-db | AND (COALESCE((fa.access_permissions->>'reports')::BOOLEAN, FALSE)
sparkyfitness-db | OR COALESCE((fa.access_permissions->>'can_view_reports')::BOOLEAN, FALSE)))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | -- Helper function to check if authenticated user is admin
sparkyfitness-db | CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public."user" u
sparkyfitness-db | WHERE u.id = authenticated_user_id()
sparkyfitness-db | AND u.role = 'admin'
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Step 4: Define generic policy creation functions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY owner_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_shared_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (%I = current_user_id());
sparkyfitness-db | ', table_name, id_column);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_diary_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_checkin_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_checkin_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''))
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_library_policy(table_name text, shared_column text, permissions text[]) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | DECLARE
sparkyfitness-db | quoted_permissions text;
sparkyfitness-db | shared_expression text;
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Quote each permission name to ensure valid ARRAY syntax
sparkyfitness-db | SELECT array_to_string(ARRAY(
sparkyfitness-db | SELECT quote_literal(p) FROM unnest(permissions) p
sparkyfitness-db | ), ',') INTO quoted_permissions;
sparkyfitness-db |
sparkyfitness-db | -- Use boolean false if shared_column is 'false', otherwise treat as column name
sparkyfitness-db | IF shared_column = 'false' THEN
sparkyfitness-db | shared_expression := 'false';
sparkyfitness-db | ELSE
sparkyfitness-db | shared_expression := quote_ident(shared_column);
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_library_access_with_public(user_id, %s, ARRAY[%s]));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | ', table_name, shared_expression, quoted_permissions, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.create_medication_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_medication_access(user_id))
sparkyfitness-db | WITH CHECK (has_medication_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 5: Apply policies to all tables.
sparkyfitness-db | -- Custom policy for ai_service_settings to support admin-global + user-owned settings
sparkyfitness-db | -- Drop ALL possible old policy names before recreating
sparkyfitness-db | DROP POLICY IF EXISTS owner_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_insert_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_update_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_delete_policy ON public.ai_service_settings;
sparkyfitness-db | -- SELECT policy: All authenticated users can read public settings, users can read their own
sparkyfitness-db | CREATE POLICY ai_service_settings_select_policy ON public.ai_service_settings FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = TRUE AND authenticated_user_id() IS NOT NULL) OR
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id())
sparkyfitness-db | );
sparkyfitness-db | -- INSERT policy: Users can create their own settings, admins can create public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_insert_policy ON public.ai_service_settings FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- UPDATE policy: Users can update their own settings, admins can update public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_update_policy ON public.ai_service_settings FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- DELETE policy: Users can delete their own settings, admins can delete public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_delete_policy ON public.ai_service_settings FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Owner-only access tables
sparkyfitness-db | -- Tier 1: Strictly Private (no delegation allowed)
sparkyfitness-db | SELECT create_owner_policy('api_key', 'reference_id');
sparkyfitness-db | SELECT create_owner_policy('user_oidc_links');
sparkyfitness-db | SELECT create_owner_policy('sparky_chat_history');
sparkyfitness-db |
sparkyfitness-db | -- Profiles: delegates can read (with any meaningful permission) but only owner can write.
sparkyfitness-db | -- Delegates do not need to modify another user's profile to manage their diary.
sparkyfitness-db | CREATE POLICY select_policy ON public.profiles FOR SELECT TO PUBLIC USING (has_profile_read_access(id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.profiles FOR ALL TO PUBLIC USING (authenticated_user_id() = id) WITH CHECK (authenticated_user_id() = id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.user_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('user_goals');
sparkyfitness-db | SELECT create_diary_policy('weekly_goal_plans');
sparkyfitness-db | SELECT create_diary_policy('user_water_containers');
sparkyfitness-db | SELECT create_diary_policy('user_custom_nutrients');
sparkyfitness-db | SELECT create_diary_policy('user_allergen_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Nutrient display preferences: delegates can read but only owner can rearrange their own columns.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_nutrient_display_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_nutrient_display_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Dashboard layouts: delegates can read but only owner can rearrange their own dashboard.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_dashboard_layouts FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_dashboard_layouts FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | SELECT create_diary_policy('goal_presets');
sparkyfitness-db | SELECT create_diary_policy('meal_plans');
sparkyfitness-db | SELECT create_checkin_policy('mood_entries');
sparkyfitness-db |
sparkyfitness-db | -- Admin Activity Logs: Only the admin who performed the action or other admins can view
sparkyfitness-db | CREATE POLICY admin_only_select ON public.admin_activity_logs FOR SELECT TO PUBLIC
sparkyfitness-db | USING (admin_user_id = current_user_id() OR is_admin());
sparkyfitness-db | CREATE POLICY admin_only_insert ON public.admin_activity_logs FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (admin_user_id = current_user_id() AND is_admin());
sparkyfitness-db |
sparkyfitness-db | -- Diary access tables
sparkyfitness-db | SELECT create_checkin_policy('check_in_measurements');
sparkyfitness-db | SELECT create_checkin_policy('check_in_photos');
sparkyfitness-db | -- Custom categories/measurements are surfaced through the check-in feature and
sparkyfitness-db | -- every /measurements route is guarded by checkPermissionMiddleware('checkin'),
sparkyfitness-db | -- so their RLS must use the check-in policy. The previous diary policy required
sparkyfitness-db | -- can_manage_diary to write, which blocked check-in delegates (e.g. the GLP-1
sparkyfitness-db | -- daily check-in) from saving even though the route allowed them.
sparkyfitness-db | SELECT create_checkin_policy('custom_categories');
sparkyfitness-db | SELECT create_checkin_policy('custom_measurements');
sparkyfitness-db | SELECT create_diary_policy('exercise_entries');
sparkyfitness-db | -- Custom policy for exercise_entries to allow access if linked to an owned exercise_preset_entry
sparkyfitness-db | CREATE POLICY select_exercise_preset_entry_linked_policy ON public.exercise_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | exercise_preset_entry_id IS NOT NULL AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.exercise_preset_entries epe
sparkyfitness-db | WHERE epe.id = exercise_entries.exercise_preset_entry_id AND has_diary_read_access(epe.user_id)
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- The modify policy for exercise_entries is already handled by create_diary_policy('exercise_entries')
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('exercise_preset_entries');
sparkyfitness-db | SELECT create_diary_policy('food_entry_meals');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entries');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entry_stages');
sparkyfitness-db | SELECT create_diary_policy('water_intake');
sparkyfitness-db | SELECT create_diary_policy('water_intake_entries');
sparkyfitness-db |
sparkyfitness-db | -- Library access tables
sparkyfitness-db | SELECT create_library_policy('exercises', 'shared_with_public', ARRAY['can_view_exercise_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('foods', 'shared_with_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meals', 'is_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meal_plan_templates', 'false', ARRAY['can_view_food_library']);
sparkyfitness-db | SELECT create_library_policy('workout_plan_templates', 'false', ARRAY['can_view_exercise_library']);
sparkyfitness-db | SELECT create_library_policy('workout_presets', 'is_public', ARRAY['can_view_exercise_library','can_manage_diary']);
sparkyfitness-db |
sparkyfitness-db | -- Medication & GLP-1 tracker (see migration 20260624000000_add_medication_glp1_schema.sql).
sparkyfitness-db | -- These tables are managed by create_medication_policy at the bottom of this file (Tier 3).
sparkyfitness-db | -- Do NOT apply create_library_policy or create_diary_policy to medication tables.
sparkyfitness-db | SELECT create_owner_policy('user_medication_display_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Cycle & Pregnancy hub (see migration 20260702180000_add_cycle_tracking_schema.sql).
sparkyfitness-db | -- Tier 1 — owner-only. Deliberately stricter than medications: this reproductive
sparkyfitness-db | -- health data is NEVER shared or delegated in v1 (no family/caregiver access).
sparkyfitness-db | SELECT create_owner_policy('cycle_settings');
sparkyfitness-db | SELECT create_owner_policy('cycle_daily_entries');
sparkyfitness-db | SELECT create_owner_policy('cycles');
sparkyfitness-db | SELECT create_owner_policy('user_cycle_display_preferences');
sparkyfitness-db | SELECT create_owner_policy('cycle_test_entries');
sparkyfitness-db |
sparkyfitness-db | -- Pregnancy mode (see migration 20260702200000_add_pregnancy_schema.sql). Tier 1
sparkyfitness-db | -- owner-only. health_appointments is generic but still owner-only in v1.
sparkyfitness-db | SELECT create_owner_policy('pregnancies');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_kick_sessions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_contractions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_photos');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_checklist_state');
sparkyfitness-db | SELECT create_owner_policy('health_appointments');
sparkyfitness-db |
sparkyfitness-db | -- User-defined mood tags. Mood is check-in data (mood_entries uses the check-in
sparkyfitness-db | -- policy), and custom check-in definitions like custom_categories are shared with
sparkyfitness-db | -- check-in delegates — so custom moods follow the same check-in policy for
sparkyfitness-db | -- consistency (a delegate managing the owner's check-in sees the owner's moods).
sparkyfitness-db | SELECT create_checkin_policy('user_custom_moods');
sparkyfitness-db |
sparkyfitness-db | -- Mood display preferences: personal picker config, owner-only.
sparkyfitness-db | SELECT create_owner_policy('user_mood_display_preferences');
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | -- Custom policies for special cases
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_read_access(ee.user_id)));
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)));
sparkyfitness-db |
sparkyfitness-db | -- Provider configs: admin-global (is_public) OR own OR family delegation
sparkyfitness-db | -- Drop any old policy names first (idempotent)
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS insert_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS update_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS delete_policy ON public.external_data_providers;
sparkyfitness-db |
sparkyfitness-db | -- SELECT: admin-global (authenticated users), own, or explicit family delegation
sparkyfitness-db | CREATE POLICY select_policy ON public.external_data_providers FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | -- Admin-created global providers: all authenticated users can see them if active
sparkyfitness-db | (is_public = TRUE AND is_active = TRUE AND authenticated_user_id() IS NOT NULL)
sparkyfitness-db | -- Owner always sees their own providers
sparkyfitness-db | OR (is_public = FALSE AND current_user_id() = user_id)
sparkyfitness-db | OR (
sparkyfitness-db | -- Explicit family delegation: share_external_providers permission, non-strictly-private only, must be active
sparkyfitness-db | is_public = FALSE AND is_active = TRUE AND has_family_access(user_id, 'share_external_providers') AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.external_provider_types ept
sparkyfitness-db | WHERE ept.id = external_data_providers.provider_type
sparkyfitness-db | AND ept.is_strictly_private = FALSE
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- INSERT: users create their own (is_public=FALSE), admins create global (is_public=TRUE)
sparkyfitness-db | CREATE POLICY insert_policy ON public.external_data_providers FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- UPDATE: users update own, admins update global
sparkyfitness-db | CREATE POLICY update_policy ON public.external_data_providers FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- DELETE: users delete own, admins delete global
sparkyfitness-db | CREATE POLICY delete_policy ON public.external_data_providers FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.family_access FOR SELECT TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id OR authenticated_user_id() = family_user_id);
sparkyfitness-db | CREATE POLICY insert_policy ON public.family_access FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db | CREATE POLICY modify_policy ON public.family_access FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY insert_policy ON public.food_entries FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | has_diary_access(user_id) AND (
sparkyfitness-db | (food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = food_entries.food_id)) OR
sparkyfitness-db | (meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = food_entries.meal_id))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY update_policy ON public.food_entries FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | CREATE POLICY delete_policy ON public.food_entries FOR DELETE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_variants FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND has_library_access_with_public(f.user_id, f.shared_with_public, ARRAY['can_view_food_library', 'can_manage_diary'])
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- Food variants are library data: only the owner of the parent food may write
sparkyfitness-db | -- them. Delegates (even can_manage_diary) get read-only access via select_policy
sparkyfitness-db | -- so they can pick serving sizes while logging, but cannot mutate the library.
sparkyfitness-db | CREATE POLICY modify_policy ON public.food_variants FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- meal_foods is polymorphic: a row references either a food (item_type='food')
sparkyfitness-db | -- or another meal (item_type='meal', a reusable sub-meal). Read access follows
sparkyfitness-db | -- the parent meal. Write access requires the caller owns the parent meal AND the
sparkyfitness-db | -- referenced ingredient is accessible to them: a food they can view, or a child
sparkyfitness-db | -- meal they have library access to (prevents linking a meal you cannot see).
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_foods FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND has_library_access_with_public(m.user_id, m.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_foods FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | AND (
sparkyfitness-db | (meal_foods.food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_foods.food_id))
sparkyfitness-db | OR
sparkyfitness-db | (meal_foods.child_meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals cm WHERE cm.id = meal_foods.child_meal_id AND has_library_access_with_public(cm.user_id, cm.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.meal_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_assignment_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercise_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_preset_exercises wpe WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercise_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercises FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_presets wp WHERE wp.id = workout_preset_exercises.workout_preset_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercises FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | -- Strictly Private (Tier 1)
sparkyfitness-db | SELECT create_owner_policy('user_ignored_updates');
sparkyfitness-db |
sparkyfitness-db | -- Meal types: access if user_id is null (system) or if user has diary access
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_types FOR SELECT TO PUBLIC
sparkyfitness-db | USING (user_id IS NULL OR has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_types FOR ALL TO PUBLIC
sparkyfitness-db | USING (user_id = authenticated_user_id())
sparkyfitness-db | WITH CHECK (user_id = authenticated_user_id());
sparkyfitness-db |
sparkyfitness-db | -- Activity details: access if linked exercise entry or preset entry is accessible
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_activity_details FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_read_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_read_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_activity_details FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Shared View-Only (Tier 2)
sparkyfitness-db | SELECT create_checkin_policy('fasting_logs');
sparkyfitness-db | SELECT create_diary_policy('user_meal_visibilities');
sparkyfitness-db | SELECT create_checkin_policy('sleep_need_calculations');
sparkyfitness-db | SELECT create_checkin_policy('daily_sleep_need');
sparkyfitness-db | -- Day classification is a sleep/wellness (check-in) feature, used only by the
sparkyfitness-db | -- sleep-science service. It must use the check-in policy so check-in delegates
sparkyfitness-db | -- can manage it; the previous diary policy was a misclassification.
sparkyfitness-db | SELECT create_checkin_policy('day_classification_cache');
sparkyfitness-db | -- Onboarding data/status: delegates can read (to avoid repeated onboarding prompts when viewing
sparkyfitness-db | -- another user's diary) but only the account owner can submit or reset their own onboarding.
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_data FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_data FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_status FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_status FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Medications & Symptoms (Tier 3 - Delegate Writable with medications permission)
sparkyfitness-db | SELECT create_medication_policy('medications');
sparkyfitness-db | SELECT create_medication_policy('medication_schedules');
sparkyfitness-db | SELECT create_medication_policy('medication_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_pens');
sparkyfitness-db | SELECT create_medication_policy('injection_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_titration_steps');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptoms');
sparkyfitness-db | SELECT create_medication_policy('symptom_entries');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptom_locations');
sparkyfitness-db |
sparkyfitness-db | -- Medications Display Preferences (Tier 2 - Owner-Only Write, Delegate Read)
sparkyfitness-db | CREATE POLICY select_policy ON public.user_medication_display_preferences FOR SELECT TO PUBLIC USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_medication_display_preferences FOR ALL TO PUBLIC USING (authenticated_user_id() = user_id) WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | 2026-11-03 02:59:38.658 CET [736] ERROR: relation "public.user_custom_symptom_locations" does not exist
sparkyfitness-db | 2026-11-03 02:59:38.658 CET [736] CONTEXT: SQL statement "ALTER TABLE public.user_custom_symptom_locations ENABLE ROW LEVEL SECURITY;"
sparkyfitness-db | PL/pgSQL function inline_code_block line 86 at EXECUTE
sparkyfitness-db | 2026-11-03 02:59:38.658 CET [736] STATEMENT: -- File: rls_policies.sql
sparkyfitness-db | -- =============================================================================
sparkyfitness-db | -- THIS FILE IS THE SINGLE SOURCE OF TRUTH FOR ALL RLS POLICIES IN THE APPLICATION.
sparkyfitness-db | -- It is executed on every server startup after migrations to ensure a consistent security state.
sparkyfitness-db | -- This script is generated from the db_schema_backup.sql to ensure all custom policies are included.
sparkyfitness-db | -- =============================================================================
sparkyfitness-db |
sparkyfitness-db | -- Step 1: Purge all existing RLS policies from the public schema in a single operation.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | pol RECORD;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR pol IN SELECT * FROM pg_policies WHERE schemaname = 'public'
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'DROP POLICY IF EXISTS ' || quote_ident(pol.policyname) || ' ON ' || quote_ident(pol.schemaname) || '.' || quote_ident(pol.tablename);
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 2: Enable RLS on all relevant tables to ensure consistent security state.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | table_name text;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR table_name IN SELECT unnest(ARRAY[
sparkyfitness-db | 'ai_service_settings',
sparkyfitness-db | 'check_in_measurements',
sparkyfitness-db | 'check_in_photos',
sparkyfitness-db | 'custom_categories',
sparkyfitness-db | 'custom_measurements',
sparkyfitness-db | 'exercise_entries',
sparkyfitness-db | 'exercise_entry_sets',
sparkyfitness-db | 'exercise_entry_activity_details',
sparkyfitness-db | 'exercises',
sparkyfitness-db | 'exercise_preset_entries',
sparkyfitness-db | 'external_data_providers',
sparkyfitness-db | 'family_access',
sparkyfitness-db | 'food_entries',
sparkyfitness-db | 'food_entry_meals',
sparkyfitness-db | 'food_variants',
sparkyfitness-db | 'foods',
sparkyfitness-db | 'goal_presets',
sparkyfitness-db | 'meal_foods',
sparkyfitness-db | 'meal_plan_template_assignments',
sparkyfitness-db | 'meal_plan_templates',
sparkyfitness-db | 'meal_plans',
sparkyfitness-db | 'meals',
sparkyfitness-db | 'meal_types',
sparkyfitness-db | 'mood_entries',
sparkyfitness-db | 'onboarding_data',
sparkyfitness-db | 'onboarding_status',
sparkyfitness-db | 'profiles',
sparkyfitness-db | 'sparky_chat_history',
sparkyfitness-db | 'admin_activity_logs',
sparkyfitness-db | 'api_key',
sparkyfitness-db | 'user_goals',
sparkyfitness-db | 'user_ignored_updates',
sparkyfitness-db | 'user_meal_visibilities',
sparkyfitness-db | 'user_nutrient_display_preferences',
sparkyfitness-db | 'user_oidc_links',
sparkyfitness-db | 'user_preferences',
sparkyfitness-db | 'user_water_containers',
sparkyfitness-db | 'water_intake',
sparkyfitness-db | 'water_intake_entries',
sparkyfitness-db | 'weekly_goal_plans',
sparkyfitness-db | 'workout_plan_assignment_sets',
sparkyfitness-db | 'workout_plan_template_assignments',
sparkyfitness-db | 'workout_plan_templates',
sparkyfitness-db | 'workout_preset_exercise_sets',
sparkyfitness-db | 'workout_preset_exercises',
sparkyfitness-db | 'workout_presets',
sparkyfitness-db | 'sleep_entries',
sparkyfitness-db | 'sleep_entry_stages',
sparkyfitness-db | 'fasting_logs',
sparkyfitness-db | 'user_custom_nutrients',
sparkyfitness-db | 'user_allergen_preferences',
sparkyfitness-db | 'user_dashboard_layouts',
sparkyfitness-db | 'sleep_need_calculations',
sparkyfitness-db | 'daily_sleep_need',
sparkyfitness-db | 'day_classification_cache',
sparkyfitness-db | 'medications',
sparkyfitness-db | 'medication_schedules',
sparkyfitness-db | 'medication_entries',
sparkyfitness-db | 'medication_pens',
sparkyfitness-db | 'injection_entries',
sparkyfitness-db | 'medication_titration_steps',
sparkyfitness-db | 'user_custom_symptoms',
sparkyfitness-db | 'symptom_entries',
sparkyfitness-db | 'user_medication_display_preferences',
sparkyfitness-db | 'user_custom_symptom_locations',
sparkyfitness-db | 'cycle_settings',
sparkyfitness-db | 'cycle_daily_entries',
sparkyfitness-db | 'cycles',
sparkyfitness-db | 'user_cycle_display_preferences',
sparkyfitness-db | 'cycle_test_entries',
sparkyfitness-db | 'pregnancies',
sparkyfitness-db | 'pregnancy_kick_sessions',
sparkyfitness-db | 'pregnancy_contractions',
sparkyfitness-db | 'pregnancy_photos',
sparkyfitness-db | 'pregnancy_checklist_state',
sparkyfitness-db | 'health_appointments',
sparkyfitness-db | 'user_custom_moods',
sparkyfitness-db | 'user_mood_display_preferences'
sparkyfitness-db | ]::text[])
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'ALTER TABLE public.' || quote_ident(table_name) || ' ENABLE ROW LEVEL SECURITY;';
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 3: Define reusable helper functions for common RLS conditions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION current_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION authenticated_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.authenticated_user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.get_accessible_users(p_user_id UUID)
sparkyfitness-db | RETURNS TABLE(
sparkyfitness-db | user_id UUID,
sparkyfitness-db | full_name TEXT,
sparkyfitness-db | email TEXT,
sparkyfitness-db | permissions JSONB,
sparkyfitness-db | access_end_date TIMESTAMP WITH TIME ZONE
sparkyfitness-db | ) AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | RETURN QUERY
sparkyfitness-db | SELECT
sparkyfitness-db | fa.owner_user_id,
sparkyfitness-db | p.full_name,
sparkyfitness-db | u.email::TEXT,
sparkyfitness-db | fa.access_permissions,
sparkyfitness-db | fa.access_end_date
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | JOIN public.profiles p ON p.id = fa.owner_user_id
sparkyfitness-db | JOIN public."user" u ON u.id = fa.owner_user_id
sparkyfitness-db | WHERE fa.family_user_id = p_user_id
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions);
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.find_user_by_email(p_email TEXT)
sparkyfitness-db | RETURNS UUID AS $func$
sparkyfitness-db | DECLARE
sparkyfitness-db | v_user_id UUID;
sparkyfitness-db | BEGIN
sparkyfitness-db | SELECT id INTO v_user_id
sparkyfitness-db | FROM public."user"
sparkyfitness-db | WHERE LOWER(email) = LOWER(p_email)
sparkyfitness-db | LIMIT 1;
sparkyfitness-db |
sparkyfitness-db | RETURN v_user_id;
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access(owner_uuid uuid, perm text) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions ->> perm)::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access_or(owner_uuid uuid, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_diary')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Centralized helper: returns true if the given permissions JSONB contains
sparkyfitness-db | -- at least one meaningful delegation permission. Used by has_profile_read_access
sparkyfitness-db | -- and get_accessible_users to avoid duplicating the permission key list.
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_any_meaningful_permission(perms jsonb) RETURNS bool
sparkyfitness-db | LANGUAGE sql IMMUTABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT (
sparkyfitness-db | (perms->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (perms->>'can_view_reports')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_profile_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | -- Owner always has access. Family delegates require at least one meaningful permission
sparkyfitness-db | -- (diary, checkin, medications, or reports) to read profile/layout/onboarding data.
sparkyfitness-db | -- A bare family_access row with no permissions does not grant read access.
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions)
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_checkin_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_read_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_medications')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_library_access_with_public(owner_uuid uuid, is_shared bool, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid
sparkyfitness-db | OR is_shared
sparkyfitness-db | OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true OR
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | AND (
sparkyfitness-db | p <> 'can_manage_diary'
sparkyfitness-db | OR current_user_id() = owner_uuid
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.set_app_context(UUID, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.set_app_context(p_user_id UUID, p_authenticated_user_id UUID)
sparkyfitness-db | RETURNS void AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- app.user_id is used by RLS to determine whose data is being accessed
sparkyfitness-db | PERFORM set_config('app.user_id', p_user_id::text, false);
sparkyfitness-db |
sparkyfitness-db | -- app.authenticated_user_id is the actual logged-in user
sparkyfitness-db | PERFORM set_config('app.authenticated_user_id', p_authenticated_user_id::text, false);
sparkyfitness-db | END;
sparkyfitness-db | $$ LANGUAGE plpgsql SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.can_access_user_data(UUID, TEXT, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.can_access_user_data(
sparkyfitness-db | target_user_id UUID,
sparkyfitness-db | permission_type TEXT,
sparkyfitness-db | auth_user_id UUID
sparkyfitness-db | ) RETURNS BOOLEAN AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Self access
sparkyfitness-db | IF target_user_id = auth_user_id THEN
sparkyfitness-db | RETURN TRUE;
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | -- Family access check
sparkyfitness-db | RETURN EXISTS (
sparkyfitness-db | SELECT 1
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | WHERE fa.family_user_id = auth_user_id
sparkyfitness-db | AND fa.owner_user_id = target_user_id
sparkyfitness-db | AND fa.is_active = TRUE
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > NOW())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>permission_type)::BOOLEAN = TRUE
sparkyfitness-db | OR
sparkyfitness-db | -- Mapping for common permission names
sparkyfitness-db | (permission_type = 'diary' AND (fa.access_permissions->>'can_manage_diary')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'checkin' AND (fa.access_permissions->>'can_manage_checkin')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'reports' AND (fa.access_permissions->>'can_view_reports')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | -- Inheritance: reports permission grants read access to others
sparkyfitness-db | (permission_type IN ('calorie', 'diary', 'mood', 'sleep', 'exercise', 'water', 'checkin')
sparkyfitness-db | AND (COALESCE((fa.access_permissions->>'reports')::BOOLEAN, FALSE)
sparkyfitness-db | OR COALESCE((fa.access_permissions->>'can_view_reports')::BOOLEAN, FALSE)))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | -- Helper function to check if authenticated user is admin
sparkyfitness-db | CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public."user" u
sparkyfitness-db | WHERE u.id = authenticated_user_id()
sparkyfitness-db | AND u.role = 'admin'
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Step 4: Define generic policy creation functions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY owner_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_shared_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (%I = current_user_id());
sparkyfitness-db | ', table_name, id_column);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_diary_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_checkin_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_checkin_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''))
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_library_policy(table_name text, shared_column text, permissions text[]) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | DECLARE
sparkyfitness-db | quoted_permissions text;
sparkyfitness-db | shared_expression text;
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Quote each permission name to ensure valid ARRAY syntax
sparkyfitness-db | SELECT array_to_string(ARRAY(
sparkyfitness-db | SELECT quote_literal(p) FROM unnest(permissions) p
sparkyfitness-db | ), ',') INTO quoted_permissions;
sparkyfitness-db |
sparkyfitness-db | -- Use boolean false if shared_column is 'false', otherwise treat as column name
sparkyfitness-db | IF shared_column = 'false' THEN
sparkyfitness-db | shared_expression := 'false';
sparkyfitness-db | ELSE
sparkyfitness-db | shared_expression := quote_ident(shared_column);
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_library_access_with_public(user_id, %s, ARRAY[%s]));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | ', table_name, shared_expression, quoted_permissions, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.create_medication_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_medication_access(user_id))
sparkyfitness-db | WITH CHECK (has_medication_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 5: Apply policies to all tables.
sparkyfitness-db | -- Custom policy for ai_service_settings to support admin-global + user-owned settings
sparkyfitness-db | -- Drop ALL possible old policy names before recreating
sparkyfitness-db | DROP POLICY IF EXISTS owner_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_insert_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_update_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_delete_policy ON public.ai_service_settings;
sparkyfitness-db | -- SELECT policy: All authenticated users can read public settings, users can read their own
sparkyfitness-db | CREATE POLICY ai_service_settings_select_policy ON public.ai_service_settings FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = TRUE AND authenticated_user_id() IS NOT NULL) OR
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id())
sparkyfitness-db | );
sparkyfitness-db | -- INSERT policy: Users can create their own settings, admins can create public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_insert_policy ON public.ai_service_settings FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- UPDATE policy: Users can update their own settings, admins can update public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_update_policy ON public.ai_service_settings FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- DELETE policy: Users can delete their own settings, admins can delete public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_delete_policy ON public.ai_service_settings FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Owner-only access tables
sparkyfitness-db | -- Tier 1: Strictly Private (no delegation allowed)
sparkyfitness-db | SELECT create_owner_policy('api_key', 'reference_id');
sparkyfitness-db | SELECT create_owner_policy('user_oidc_links');
sparkyfitness-db | SELECT create_owner_policy('sparky_chat_history');
sparkyfitness-db |
sparkyfitness-db | -- Profiles: delegates can read (with any meaningful permission) but only owner can write.
sparkyfitness-db | -- Delegates do not need to modify another user's profile to manage their diary.
sparkyfitness-db | CREATE POLICY select_policy ON public.profiles FOR SELECT TO PUBLIC USING (has_profile_read_access(id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.profiles FOR ALL TO PUBLIC USING (authenticated_user_id() = id) WITH CHECK (authenticated_user_id() = id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.user_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('user_goals');
sparkyfitness-db | SELECT create_diary_policy('weekly_goal_plans');
sparkyfitness-db | SELECT create_diary_policy('user_water_containers');
sparkyfitness-db | SELECT create_diary_policy('user_custom_nutrients');
sparkyfitness-db | SELECT create_diary_policy('user_allergen_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Nutrient display preferences: delegates can read but only owner can rearrange their own columns.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_nutrient_display_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_nutrient_display_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Dashboard layouts: delegates can read but only owner can rearrange their own dashboard.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_dashboard_layouts FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_dashboard_layouts FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | SELECT create_diary_policy('goal_presets');
sparkyfitness-db | SELECT create_diary_policy('meal_plans');
sparkyfitness-db | SELECT create_checkin_policy('mood_entries');
sparkyfitness-db |
sparkyfitness-db | -- Admin Activity Logs: Only the admin who performed the action or other admins can view
sparkyfitness-db | CREATE POLICY admin_only_select ON public.admin_activity_logs FOR SELECT TO PUBLIC
sparkyfitness-db | USING (admin_user_id = current_user_id() OR is_admin());
sparkyfitness-db | CREATE POLICY admin_only_insert ON public.admin_activity_logs FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (admin_user_id = current_user_id() AND is_admin());
sparkyfitness-db |
sparkyfitness-db | -- Diary access tables
sparkyfitness-db | SELECT create_checkin_policy('check_in_measurements');
sparkyfitness-db | SELECT create_checkin_policy('check_in_photos');
sparkyfitness-db | -- Custom categories/measurements are surfaced through the check-in feature and
sparkyfitness-db | -- every /measurements route is guarded by checkPermissionMiddleware('checkin'),
sparkyfitness-db | -- so their RLS must use the check-in policy. The previous diary policy required
sparkyfitness-db | -- can_manage_diary to write, which blocked check-in delegates (e.g. the GLP-1
sparkyfitness-db | -- daily check-in) from saving even though the route allowed them.
sparkyfitness-db | SELECT create_checkin_policy('custom_categories');
sparkyfitness-db | SELECT create_checkin_policy('custom_measurements');
sparkyfitness-db | SELECT create_diary_policy('exercise_entries');
sparkyfitness-db | -- Custom policy for exercise_entries to allow access if linked to an owned exercise_preset_entry
sparkyfitness-db | CREATE POLICY select_exercise_preset_entry_linked_policy ON public.exercise_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | exercise_preset_entry_id IS NOT NULL AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.exercise_preset_entries epe
sparkyfitness-db | WHERE epe.id = exercise_entries.exercise_preset_entry_id AND has_diary_read_access(epe.user_id)
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- The modify policy for exercise_entries is already handled by create_diary_policy('exercise_entries')
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('exercise_preset_entries');
sparkyfitness-db | SELECT create_diary_policy('food_entry_meals');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entries');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entry_stages');
sparkyfitness-db | SELECT create_diary_policy('water_intake');
sparkyfitness-db | SELECT create_diary_policy('water_intake_entries');
sparkyfitness-db |
sparkyfitness-db | -- Library access tables
sparkyfitness-db | SELECT create_library_policy('exercises', 'shared_with_public', ARRAY['can_view_exercise_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('foods', 'shared_with_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meals', 'is_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meal_plan_templates', 'false', ARRAY['can_view_food_library']);
sparkyfitness-db | SELECT create_library_policy('workout_plan_templates', 'false', ARRAY['can_view_exercise_library']);
sparkyfitness-db | SELECT create_library_policy('workout_presets', 'is_public', ARRAY['can_view_exercise_library','can_manage_diary']);
sparkyfitness-db |
sparkyfitness-db | -- Medication & GLP-1 tracker (see migration 20260624000000_add_medication_glp1_schema.sql).
sparkyfitness-db | -- These tables are managed by create_medication_policy at the bottom of this file (Tier 3).
sparkyfitness-db | -- Do NOT apply create_library_policy or create_diary_policy to medication tables.
sparkyfitness-db | SELECT create_owner_policy('user_medication_display_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Cycle & Pregnancy hub (see migration 20260702180000_add_cycle_tracking_schema.sql).
sparkyfitness-db | -- Tier 1 — owner-only. Deliberately stricter than medications: this reproductive
sparkyfitness-db | -- health data is NEVER shared or delegated in v1 (no family/caregiver access).
sparkyfitness-db | SELECT create_owner_policy('cycle_settings');
sparkyfitness-db | SELECT create_owner_policy('cycle_daily_entries');
sparkyfitness-db | SELECT create_owner_policy('cycles');
sparkyfitness-db | SELECT create_owner_policy('user_cycle_display_preferences');
sparkyfitness-db | SELECT create_owner_policy('cycle_test_entries');
sparkyfitness-db |
sparkyfitness-db | -- Pregnancy mode (see migration 20260702200000_add_pregnancy_schema.sql). Tier 1
sparkyfitness-db | -- owner-only. health_appointments is generic but still owner-only in v1.
sparkyfitness-db | SELECT create_owner_policy('pregnancies');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_kick_sessions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_contractions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_photos');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_checklist_state');
sparkyfitness-db | SELECT create_owner_policy('health_appointments');
sparkyfitness-db |
sparkyfitness-db | -- User-defined mood tags. Mood is check-in data (mood_entries uses the check-in
sparkyfitness-db | -- policy), and custom check-in definitions like custom_categories are shared with
sparkyfitness-db | -- check-in delegates — so custom moods follow the same check-in policy for
sparkyfitness-db | -- consistency (a delegate managing the owner's check-in sees the owner's moods).
sparkyfitness-db | SELECT create_checkin_policy('user_custom_moods');
sparkyfitness-db |
sparkyfitness-db | -- Mood display preferences: personal picker config, owner-only.
sparkyfitness-db | SELECT create_owner_policy('user_mood_display_preferences');
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | -- Custom policies for special cases
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_read_access(ee.user_id)));
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)));
sparkyfitness-db |
sparkyfitness-db | -- Provider configs: admin-global (is_public) OR own OR family delegation
sparkyfitness-db | -- Drop any old policy names first (idempotent)
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS insert_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS update_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS delete_policy ON public.external_data_providers;
sparkyfitness-db |
sparkyfitness-db | -- SELECT: admin-global (authenticated users), own, or explicit family delegation
sparkyfitness-db | CREATE POLICY select_policy ON public.external_data_providers FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | -- Admin-created global providers: all authenticated users can see them if active
sparkyfitness-db | (is_public = TRUE AND is_active = TRUE AND authenticated_user_id() IS NOT NULL)
sparkyfitness-db | -- Owner always sees their own providers
sparkyfitness-db | OR (is_public = FALSE AND current_user_id() = user_id)
sparkyfitness-db | OR (
sparkyfitness-db | -- Explicit family delegation: share_external_providers permission, non-strictly-private only, must be active
sparkyfitness-db | is_public = FALSE AND is_active = TRUE AND has_family_access(user_id, 'share_external_providers') AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.external_provider_types ept
sparkyfitness-db | WHERE ept.id = external_data_providers.provider_type
sparkyfitness-db | AND ept.is_strictly_private = FALSE
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- INSERT: users create their own (is_public=FALSE), admins create global (is_public=TRUE)
sparkyfitness-db | CREATE POLICY insert_policy ON public.external_data_providers FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- UPDATE: users update own, admins update global
sparkyfitness-db | CREATE POLICY update_policy ON public.external_data_providers FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- DELETE: users delete own, admins delete global
sparkyfitness-db | CREATE POLICY delete_policy ON public.external_data_providers FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.family_access FOR SELECT TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id OR authenticated_user_id() = family_user_id);
sparkyfitness-db | CREATE POLICY insert_policy ON public.family_access FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db | CREATE POLICY modify_policy ON public.family_access FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY insert_policy ON public.food_entries FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | has_diary_access(user_id) AND (
sparkyfitness-db | (food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = food_entries.food_id)) OR
sparkyfitness-db | (meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = food_entries.meal_id))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY update_policy ON public.food_entries FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | CREATE POLICY delete_policy ON public.food_entries FOR DELETE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_variants FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND has_library_access_with_public(f.user_id, f.shared_with_public, ARRAY['can_view_food_library', 'can_manage_diary'])
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- Food variants are library data: only the owner of the parent food may write
sparkyfitness-db | -- them. Delegates (even can_manage_diary) get read-only access via select_policy
sparkyfitness-db | -- so they can pick serving sizes while logging, but cannot mutate the library.
sparkyfitness-db | CREATE POLICY modify_policy ON public.food_variants FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- meal_foods is polymorphic: a row references either a food (item_type='food')
sparkyfitness-db | -- or another meal (item_type='meal', a reusable sub-meal). Read access follows
sparkyfitness-db | -- the parent meal. Write access requires the caller owns the parent meal AND the
sparkyfitness-db | -- referenced ingredient is accessible to them: a food they can view, or a child
sparkyfitness-db | -- meal they have library access to (prevents linking a meal you cannot see).
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_foods FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND has_library_access_with_public(m.user_id, m.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_foods FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | AND (
sparkyfitness-db | (meal_foods.food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_foods.food_id))
sparkyfitness-db | OR
sparkyfitness-db | (meal_foods.child_meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals cm WHERE cm.id = meal_foods.child_meal_id AND has_library_access_with_public(cm.user_id, cm.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.meal_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_assignment_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercise_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_preset_exercises wpe WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercise_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercises FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_presets wp WHERE wp.id = workout_preset_exercises.workout_preset_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercises FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | -- Strictly Private (Tier 1)
sparkyfitness-db | SELECT create_owner_policy('user_ignored_updates');
sparkyfitness-db |
sparkyfitness-db | -- Meal types: access if user_id is null (system) or if user has diary access
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_types FOR SELECT TO PUBLIC
sparkyfitness-db | USING (user_id IS NULL OR has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_types FOR ALL TO PUBLIC
sparkyfitness-db | USING (user_id = authenticated_user_id())
sparkyfitness-db | WITH CHECK (user_id = authenticated_user_id());
sparkyfitness-db |
sparkyfitness-db | -- Activity details: access if linked exercise entry or preset entry is accessible
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_activity_details FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_read_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_read_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_activity_details FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Shared View-Only (Tier 2)
sparkyfitness-db | SELECT create_checkin_policy('fasting_logs');
sparkyfitness-db | SELECT create_diary_policy('user_meal_visibilities');
sparkyfitness-db | SELECT create_checkin_policy('sleep_need_calculations');
sparkyfitness-db | SELECT create_checkin_policy('daily_sleep_need');
sparkyfitness-db | -- Day classification is a sleep/wellness (check-in) feature, used only by the
sparkyfitness-db | -- sleep-science service. It must use the check-in policy so check-in delegates
sparkyfitness-db | -- can manage it; the previous diary policy was a misclassification.
sparkyfitness-db | SELECT create_checkin_policy('day_classification_cache');
sparkyfitness-db | -- Onboarding data/status: delegates can read (to avoid repeated onboarding prompts when viewing
sparkyfitness-db | -- another user's diary) but only the account owner can submit or reset their own onboarding.
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_data FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_data FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_status FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_status FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Medications & Symptoms (Tier 3 - Delegate Writable with medications permission)
sparkyfitness-db | SELECT create_medication_policy('medications');
sparkyfitness-db | SELECT create_medication_policy('medication_schedules');
sparkyfitness-db | SELECT create_medication_policy('medication_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_pens');
sparkyfitness-db | SELECT create_medication_policy('injection_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_titration_steps');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptoms');
sparkyfitness-db | SELECT create_medication_policy('symptom_entries');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptom_locations');
sparkyfitness-db |
sparkyfitness-db | -- Medications Display Preferences (Tier 2 - Owner-Only Write, Delegate Read)
sparkyfitness-db | CREATE POLICY select_policy ON public.user_medication_display_preferences FOR SELECT TO PUBLIC USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_medication_display_preferences FOR ALL TO PUBLIC USING (authenticated_user_id() = user_id) WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | 2026-11-03 03:00:07.176 CET [62] LOG: checkpoint starting: time
sparkyfitness-db | 2026-11-03 03:00:18.352 CET [768] ERROR: relation "public.user_custom_symptom_locations" does not exist
sparkyfitness-db | 2026-11-03 03:00:18.352 CET [768] CONTEXT: SQL statement "ALTER TABLE public.user_custom_symptom_locations ENABLE ROW LEVEL SECURITY;"
sparkyfitness-db | PL/pgSQL function inline_code_block line 86 at EXECUTE
sparkyfitness-db | 2026-11-03 03:00:18.352 CET [768] STATEMENT: -- File: rls_policies.sql
sparkyfitness-db | -- =============================================================================
sparkyfitness-db | -- THIS FILE IS THE SINGLE SOURCE OF TRUTH FOR ALL RLS POLICIES IN THE APPLICATION.
sparkyfitness-db | -- It is executed on every server startup after migrations to ensure a consistent security state.
sparkyfitness-db | -- This script is generated from the db_schema_backup.sql to ensure all custom policies are included.
sparkyfitness-db | -- =============================================================================
sparkyfitness-db |
sparkyfitness-db | -- Step 1: Purge all existing RLS policies from the public schema in a single operation.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | pol RECORD;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR pol IN SELECT * FROM pg_policies WHERE schemaname = 'public'
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'DROP POLICY IF EXISTS ' || quote_ident(pol.policyname) || ' ON ' || quote_ident(pol.schemaname) || '.' || quote_ident(pol.tablename);
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 2: Enable RLS on all relevant tables to ensure consistent security state.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | table_name text;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR table_name IN SELECT unnest(ARRAY[
sparkyfitness-db | 'ai_service_settings',
sparkyfitness-db | 'check_in_measurements',
sparkyfitness-db | 'check_in_photos',
sparkyfitness-db | 'custom_categories',
sparkyfitness-db | 'custom_measurements',
sparkyfitness-db | 'exercise_entries',
sparkyfitness-db | 'exercise_entry_sets',
sparkyfitness-db | 'exercise_entry_activity_details',
sparkyfitness-db | 'exercises',
sparkyfitness-db | 'exercise_preset_entries',
sparkyfitness-db | 'external_data_providers',
sparkyfitness-db | 'family_access',
sparkyfitness-db | 'food_entries',
sparkyfitness-db | 'food_entry_meals',
sparkyfitness-db | 'food_variants',
sparkyfitness-db | 'foods',
sparkyfitness-db | 'goal_presets',
sparkyfitness-db | 'meal_foods',
sparkyfitness-db | 'meal_plan_template_assignments',
sparkyfitness-db | 'meal_plan_templates',
sparkyfitness-db | 'meal_plans',
sparkyfitness-db | 'meals',
sparkyfitness-db | 'meal_types',
sparkyfitness-db | 'mood_entries',
sparkyfitness-db | 'onboarding_data',
sparkyfitness-db | 'onboarding_status',
sparkyfitness-db | 'profiles',
sparkyfitness-db | 'sparky_chat_history',
sparkyfitness-db | 'admin_activity_logs',
sparkyfitness-db | 'api_key',
sparkyfitness-db | 'user_goals',
sparkyfitness-db | 'user_ignored_updates',
sparkyfitness-db | 'user_meal_visibilities',
sparkyfitness-db | 'user_nutrient_display_preferences',
sparkyfitness-db | 'user_oidc_links',
sparkyfitness-db | 'user_preferences',
sparkyfitness-db | 'user_water_containers',
sparkyfitness-db | 'water_intake',
sparkyfitness-db | 'water_intake_entries',
sparkyfitness-db | 'weekly_goal_plans',
sparkyfitness-db | 'workout_plan_assignment_sets',
sparkyfitness-db | 'workout_plan_template_assignments',
sparkyfitness-db | 'workout_plan_templates',
sparkyfitness-db | 'workout_preset_exercise_sets',
sparkyfitness-db | 'workout_preset_exercises',
sparkyfitness-db | 'workout_presets',
sparkyfitness-db | 'sleep_entries',
sparkyfitness-db | 'sleep_entry_stages',
sparkyfitness-db | 'fasting_logs',
sparkyfitness-db | 'user_custom_nutrients',
sparkyfitness-db | 'user_allergen_preferences',
sparkyfitness-db | 'user_dashboard_layouts',
sparkyfitness-db | 'sleep_need_calculations',
sparkyfitness-db | 'daily_sleep_need',
sparkyfitness-db | 'day_classification_cache',
sparkyfitness-db | 'medications',
sparkyfitness-db | 'medication_schedules',
sparkyfitness-db | 'medication_entries',
sparkyfitness-db | 'medication_pens',
sparkyfitness-db | 'injection_entries',
sparkyfitness-db | 'medication_titration_steps',
sparkyfitness-db | 'user_custom_symptoms',
sparkyfitness-db | 'symptom_entries',
sparkyfitness-db | 'user_medication_display_preferences',
sparkyfitness-db | 'user_custom_symptom_locations',
sparkyfitness-db | 'cycle_settings',
sparkyfitness-db | 'cycle_daily_entries',
sparkyfitness-db | 'cycles',
sparkyfitness-db | 'user_cycle_display_preferences',
sparkyfitness-db | 'cycle_test_entries',
sparkyfitness-db | 'pregnancies',
sparkyfitness-db | 'pregnancy_kick_sessions',
sparkyfitness-db | 'pregnancy_contractions',
sparkyfitness-db | 'pregnancy_photos',
sparkyfitness-db | 'pregnancy_checklist_state',
sparkyfitness-db | 'health_appointments',
sparkyfitness-db | 'user_custom_moods',
sparkyfitness-db | 'user_mood_display_preferences'
sparkyfitness-db | ]::text[])
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'ALTER TABLE public.' || quote_ident(table_name) || ' ENABLE ROW LEVEL SECURITY;';
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 3: Define reusable helper functions for common RLS conditions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION current_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION authenticated_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.authenticated_user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.get_accessible_users(p_user_id UUID)
sparkyfitness-db | RETURNS TABLE(
sparkyfitness-db | user_id UUID,
sparkyfitness-db | full_name TEXT,
sparkyfitness-db | email TEXT,
sparkyfitness-db | permissions JSONB,
sparkyfitness-db | access_end_date TIMESTAMP WITH TIME ZONE
sparkyfitness-db | ) AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | RETURN QUERY
sparkyfitness-db | SELECT
sparkyfitness-db | fa.owner_user_id,
sparkyfitness-db | p.full_name,
sparkyfitness-db | u.email::TEXT,
sparkyfitness-db | fa.access_permissions,
sparkyfitness-db | fa.access_end_date
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | JOIN public.profiles p ON p.id = fa.owner_user_id
sparkyfitness-db | JOIN public."user" u ON u.id = fa.owner_user_id
sparkyfitness-db | WHERE fa.family_user_id = p_user_id
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions);
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.find_user_by_email(p_email TEXT)
sparkyfitness-db | RETURNS UUID AS $func$
sparkyfitness-db | DECLARE
sparkyfitness-db | v_user_id UUID;
sparkyfitness-db | BEGIN
sparkyfitness-db | SELECT id INTO v_user_id
sparkyfitness-db | FROM public."user"
sparkyfitness-db | WHERE LOWER(email) = LOWER(p_email)
sparkyfitness-db | LIMIT 1;
sparkyfitness-db |
sparkyfitness-db | RETURN v_user_id;
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access(owner_uuid uuid, perm text) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions ->> perm)::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access_or(owner_uuid uuid, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_diary')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Centralized helper: returns true if the given permissions JSONB contains
sparkyfitness-db | -- at least one meaningful delegation permission. Used by has_profile_read_access
sparkyfitness-db | -- and get_accessible_users to avoid duplicating the permission key list.
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_any_meaningful_permission(perms jsonb) RETURNS bool
sparkyfitness-db | LANGUAGE sql IMMUTABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT (
sparkyfitness-db | (perms->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (perms->>'can_view_reports')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_profile_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | -- Owner always has access. Family delegates require at least one meaningful permission
sparkyfitness-db | -- (diary, checkin, medications, or reports) to read profile/layout/onboarding data.
sparkyfitness-db | -- A bare family_access row with no permissions does not grant read access.
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions)
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_checkin_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_read_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_medications')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_library_access_with_public(owner_uuid uuid, is_shared bool, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid
sparkyfitness-db | OR is_shared
sparkyfitness-db | OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true OR
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | AND (
sparkyfitness-db | p <> 'can_manage_diary'
sparkyfitness-db | OR current_user_id() = owner_uuid
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.set_app_context(UUID, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.set_app_context(p_user_id UUID, p_authenticated_user_id UUID)
sparkyfitness-db | RETURNS void AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- app.user_id is used by RLS to determine whose data is being accessed
sparkyfitness-db | PERFORM set_config('app.user_id', p_user_id::text, false);
sparkyfitness-db |
sparkyfitness-db | -- app.authenticated_user_id is the actual logged-in user
sparkyfitness-db | PERFORM set_config('app.authenticated_user_id', p_authenticated_user_id::text, false);
sparkyfitness-db | END;
sparkyfitness-db | $$ LANGUAGE plpgsql SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.can_access_user_data(UUID, TEXT, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.can_access_user_data(
sparkyfitness-db | target_user_id UUID,
sparkyfitness-db | permission_type TEXT,
sparkyfitness-db | auth_user_id UUID
sparkyfitness-db | ) RETURNS BOOLEAN AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Self access
sparkyfitness-db | IF target_user_id = auth_user_id THEN
sparkyfitness-db | RETURN TRUE;
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | -- Family access check
sparkyfitness-db | RETURN EXISTS (
sparkyfitness-db | SELECT 1
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | WHERE fa.family_user_id = auth_user_id
sparkyfitness-db | AND fa.owner_user_id = target_user_id
sparkyfitness-db | AND fa.is_active = TRUE
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > NOW())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>permission_type)::BOOLEAN = TRUE
sparkyfitness-db | OR
sparkyfitness-db | -- Mapping for common permission names
sparkyfitness-db | (permission_type = 'diary' AND (fa.access_permissions->>'can_manage_diary')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'checkin' AND (fa.access_permissions->>'can_manage_checkin')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'reports' AND (fa.access_permissions->>'can_view_reports')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | -- Inheritance: reports permission grants read access to others
sparkyfitness-db | (permission_type IN ('calorie', 'diary', 'mood', 'sleep', 'exercise', 'water', 'checkin')
sparkyfitness-db | AND (COALESCE((fa.access_permissions->>'reports')::BOOLEAN, FALSE)
sparkyfitness-db | OR COALESCE((fa.access_permissions->>'can_view_reports')::BOOLEAN, FALSE)))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | -- Helper function to check if authenticated user is admin
sparkyfitness-db | CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public."user" u
sparkyfitness-db | WHERE u.id = authenticated_user_id()
sparkyfitness-db | AND u.role = 'admin'
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Step 4: Define generic policy creation functions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY owner_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_shared_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (%I = current_user_id());
sparkyfitness-db | ', table_name, id_column);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_diary_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_checkin_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_checkin_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''))
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_library_policy(table_name text, shared_column text, permissions text[]) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | DECLARE
sparkyfitness-db | quoted_permissions text;
sparkyfitness-db | shared_expression text;
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Quote each permission name to ensure valid ARRAY syntax
sparkyfitness-db | SELECT array_to_string(ARRAY(
sparkyfitness-db | SELECT quote_literal(p) FROM unnest(permissions) p
sparkyfitness-db | ), ',') INTO quoted_permissions;
sparkyfitness-db |
sparkyfitness-db | -- Use boolean false if shared_column is 'false', otherwise treat as column name
sparkyfitness-db | IF shared_column = 'false' THEN
sparkyfitness-db | shared_expression := 'false';
sparkyfitness-db | ELSE
sparkyfitness-db | shared_expression := quote_ident(shared_column);
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_library_access_with_public(user_id, %s, ARRAY[%s]));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | ', table_name, shared_expression, quoted_permissions, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.create_medication_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_medication_access(user_id))
sparkyfitness-db | WITH CHECK (has_medication_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 5: Apply policies to all tables.
sparkyfitness-db | -- Custom policy for ai_service_settings to support admin-global + user-owned settings
sparkyfitness-db | -- Drop ALL possible old policy names before recreating
sparkyfitness-db | DROP POLICY IF EXISTS owner_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_insert_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_update_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_delete_policy ON public.ai_service_settings;
sparkyfitness-db | -- SELECT policy: All authenticated users can read public settings, users can read their own
sparkyfitness-db | CREATE POLICY ai_service_settings_select_policy ON public.ai_service_settings FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = TRUE AND authenticated_user_id() IS NOT NULL) OR
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id())
sparkyfitness-db | );
sparkyfitness-db | -- INSERT policy: Users can create their own settings, admins can create public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_insert_policy ON public.ai_service_settings FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- UPDATE policy: Users can update their own settings, admins can update public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_update_policy ON public.ai_service_settings FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- DELETE policy: Users can delete their own settings, admins can delete public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_delete_policy ON public.ai_service_settings FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Owner-only access tables
sparkyfitness-db | -- Tier 1: Strictly Private (no delegation allowed)
sparkyfitness-db | SELECT create_owner_policy('api_key', 'reference_id');
sparkyfitness-db | SELECT create_owner_policy('user_oidc_links');
sparkyfitness-db | SELECT create_owner_policy('sparky_chat_history');
sparkyfitness-db |
sparkyfitness-db | -- Profiles: delegates can read (with any meaningful permission) but only owner can write.
sparkyfitness-db | -- Delegates do not need to modify another user's profile to manage their diary.
sparkyfitness-db | CREATE POLICY select_policy ON public.profiles FOR SELECT TO PUBLIC USING (has_profile_read_access(id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.profiles FOR ALL TO PUBLIC USING (authenticated_user_id() = id) WITH CHECK (authenticated_user_id() = id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.user_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('user_goals');
sparkyfitness-db | SELECT create_diary_policy('weekly_goal_plans');
sparkyfitness-db | SELECT create_diary_policy('user_water_containers');
sparkyfitness-db | SELECT create_diary_policy('user_custom_nutrients');
sparkyfitness-db | SELECT create_diary_policy('user_allergen_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Nutrient display preferences: delegates can read but only owner can rearrange their own columns.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_nutrient_display_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_nutrient_display_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Dashboard layouts: delegates can read but only owner can rearrange their own dashboard.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_dashboard_layouts FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_dashboard_layouts FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | SELECT create_diary_policy('goal_presets');
sparkyfitness-db | SELECT create_diary_policy('meal_plans');
sparkyfitness-db | SELECT create_checkin_policy('mood_entries');
sparkyfitness-db |
sparkyfitness-db | -- Admin Activity Logs: Only the admin who performed the action or other admins can view
sparkyfitness-db | CREATE POLICY admin_only_select ON public.admin_activity_logs FOR SELECT TO PUBLIC
sparkyfitness-db | USING (admin_user_id = current_user_id() OR is_admin());
sparkyfitness-db | CREATE POLICY admin_only_insert ON public.admin_activity_logs FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (admin_user_id = current_user_id() AND is_admin());
sparkyfitness-db |
sparkyfitness-db | -- Diary access tables
sparkyfitness-db | SELECT create_checkin_policy('check_in_measurements');
sparkyfitness-db | SELECT create_checkin_policy('check_in_photos');
sparkyfitness-db | -- Custom categories/measurements are surfaced through the check-in feature and
sparkyfitness-db | -- every /measurements route is guarded by checkPermissionMiddleware('checkin'),
sparkyfitness-db | -- so their RLS must use the check-in policy. The previous diary policy required
sparkyfitness-db | -- can_manage_diary to write, which blocked check-in delegates (e.g. the GLP-1
sparkyfitness-db | -- daily check-in) from saving even though the route allowed them.
sparkyfitness-db | SELECT create_checkin_policy('custom_categories');
sparkyfitness-db | SELECT create_checkin_policy('custom_measurements');
sparkyfitness-db | SELECT create_diary_policy('exercise_entries');
sparkyfitness-db | -- Custom policy for exercise_entries to allow access if linked to an owned exercise_preset_entry
sparkyfitness-db | CREATE POLICY select_exercise_preset_entry_linked_policy ON public.exercise_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | exercise_preset_entry_id IS NOT NULL AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.exercise_preset_entries epe
sparkyfitness-db | WHERE epe.id = exercise_entries.exercise_preset_entry_id AND has_diary_read_access(epe.user_id)
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- The modify policy for exercise_entries is already handled by create_diary_policy('exercise_entries')
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('exercise_preset_entries');
sparkyfitness-db | SELECT create_diary_policy('food_entry_meals');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entries');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entry_stages');
sparkyfitness-db | SELECT create_diary_policy('water_intake');
sparkyfitness-db | SELECT create_diary_policy('water_intake_entries');
sparkyfitness-db |
sparkyfitness-db | -- Library access tables
sparkyfitness-db | SELECT create_library_policy('exercises', 'shared_with_public', ARRAY['can_view_exercise_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('foods', 'shared_with_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meals', 'is_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meal_plan_templates', 'false', ARRAY['can_view_food_library']);
sparkyfitness-db | SELECT create_library_policy('workout_plan_templates', 'false', ARRAY['can_view_exercise_library']);
sparkyfitness-db | SELECT create_library_policy('workout_presets', 'is_public', ARRAY['can_view_exercise_library','can_manage_diary']);
sparkyfitness-db |
sparkyfitness-db | -- Medication & GLP-1 tracker (see migration 20260624000000_add_medication_glp1_schema.sql).
sparkyfitness-db | -- These tables are managed by create_medication_policy at the bottom of this file (Tier 3).
sparkyfitness-db | -- Do NOT apply create_library_policy or create_diary_policy to medication tables.
sparkyfitness-db | SELECT create_owner_policy('user_medication_display_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Cycle & Pregnancy hub (see migration 20260702180000_add_cycle_tracking_schema.sql).
sparkyfitness-db | -- Tier 1 — owner-only. Deliberately stricter than medications: this reproductive
sparkyfitness-db | -- health data is NEVER shared or delegated in v1 (no family/caregiver access).
sparkyfitness-db | SELECT create_owner_policy('cycle_settings');
sparkyfitness-db | SELECT create_owner_policy('cycle_daily_entries');
sparkyfitness-db | SELECT create_owner_policy('cycles');
sparkyfitness-db | SELECT create_owner_policy('user_cycle_display_preferences');
sparkyfitness-db | SELECT create_owner_policy('cycle_test_entries');
sparkyfitness-db |
sparkyfitness-db | -- Pregnancy mode (see migration 20260702200000_add_pregnancy_schema.sql). Tier 1
sparkyfitness-db | -- owner-only. health_appointments is generic but still owner-only in v1.
sparkyfitness-db | SELECT create_owner_policy('pregnancies');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_kick_sessions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_contractions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_photos');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_checklist_state');
sparkyfitness-db | SELECT create_owner_policy('health_appointments');
sparkyfitness-db |
sparkyfitness-db | -- User-defined mood tags. Mood is check-in data (mood_entries uses the check-in
sparkyfitness-db | -- policy), and custom check-in definitions like custom_categories are shared with
sparkyfitness-db | -- check-in delegates — so custom moods follow the same check-in policy for
sparkyfitness-db | -- consistency (a delegate managing the owner's check-in sees the owner's moods).
sparkyfitness-db | SELECT create_checkin_policy('user_custom_moods');
sparkyfitness-db |
sparkyfitness-db | -- Mood display preferences: personal picker config, owner-only.
sparkyfitness-db | SELECT create_owner_policy('user_mood_display_preferences');
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | -- Custom policies for special cases
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_read_access(ee.user_id)));
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)));
sparkyfitness-db |
sparkyfitness-db | -- Provider configs: admin-global (is_public) OR own OR family delegation
sparkyfitness-db | -- Drop any old policy names first (idempotent)
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS insert_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS update_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS delete_policy ON public.external_data_providers;
sparkyfitness-db |
sparkyfitness-db | -- SELECT: admin-global (authenticated users), own, or explicit family delegation
sparkyfitness-db | CREATE POLICY select_policy ON public.external_data_providers FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | -- Admin-created global providers: all authenticated users can see them if active
sparkyfitness-db | (is_public = TRUE AND is_active = TRUE AND authenticated_user_id() IS NOT NULL)
sparkyfitness-db | -- Owner always sees their own providers
sparkyfitness-db | OR (is_public = FALSE AND current_user_id() = user_id)
sparkyfitness-db | OR (
sparkyfitness-db | -- Explicit family delegation: share_external_providers permission, non-strictly-private only, must be active
sparkyfitness-db | is_public = FALSE AND is_active = TRUE AND has_family_access(user_id, 'share_external_providers') AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.external_provider_types ept
sparkyfitness-db | WHERE ept.id = external_data_providers.provider_type
sparkyfitness-db | AND ept.is_strictly_private = FALSE
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- INSERT: users create their own (is_public=FALSE), admins create global (is_public=TRUE)
sparkyfitness-db | CREATE POLICY insert_policy ON public.external_data_providers FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- UPDATE: users update own, admins update global
sparkyfitness-db | CREATE POLICY update_policy ON public.external_data_providers FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- DELETE: users delete own, admins delete global
sparkyfitness-db | CREATE POLICY delete_policy ON public.external_data_providers FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.family_access FOR SELECT TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id OR authenticated_user_id() = family_user_id);
sparkyfitness-db | CREATE POLICY insert_policy ON public.family_access FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db | CREATE POLICY modify_policy ON public.family_access FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY insert_policy ON public.food_entries FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | has_diary_access(user_id) AND (
sparkyfitness-db | (food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = food_entries.food_id)) OR
sparkyfitness-db | (meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = food_entries.meal_id))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY update_policy ON public.food_entries FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | CREATE POLICY delete_policy ON public.food_entries FOR DELETE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_variants FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND has_library_access_with_public(f.user_id, f.shared_with_public, ARRAY['can_view_food_library', 'can_manage_diary'])
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- Food variants are library data: only the owner of the parent food may write
sparkyfitness-db | -- them. Delegates (even can_manage_diary) get read-only access via select_policy
sparkyfitness-db | -- so they can pick serving sizes while logging, but cannot mutate the library.
sparkyfitness-db | CREATE POLICY modify_policy ON public.food_variants FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- meal_foods is polymorphic: a row references either a food (item_type='food')
sparkyfitness-db | -- or another meal (item_type='meal', a reusable sub-meal). Read access follows
sparkyfitness-db | -- the parent meal. Write access requires the caller owns the parent meal AND the
sparkyfitness-db | -- referenced ingredient is accessible to them: a food they can view, or a child
sparkyfitness-db | -- meal they have library access to (prevents linking a meal you cannot see).
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_foods FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND has_library_access_with_public(m.user_id, m.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_foods FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | AND (
sparkyfitness-db | (meal_foods.food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_foods.food_id))
sparkyfitness-db | OR
sparkyfitness-db | (meal_foods.child_meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals cm WHERE cm.id = meal_foods.child_meal_id AND has_library_access_with_public(cm.user_id, cm.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.meal_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_assignment_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercise_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_preset_exercises wpe WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercise_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercises FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_presets wp WHERE wp.id = workout_preset_exercises.workout_preset_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercises FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | -- Strictly Private (Tier 1)
sparkyfitness-db | SELECT create_owner_policy('user_ignored_updates');
sparkyfitness-db |
sparkyfitness-db | -- Meal types: access if user_id is null (system) or if user has diary access
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_types FOR SELECT TO PUBLIC
sparkyfitness-db | USING (user_id IS NULL OR has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_types FOR ALL TO PUBLIC
sparkyfitness-db | USING (user_id = authenticated_user_id())
sparkyfitness-db | WITH CHECK (user_id = authenticated_user_id());
sparkyfitness-db |
sparkyfitness-db | -- Activity details: access if linked exercise entry or preset entry is accessible
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_activity_details FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_read_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_read_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_activity_details FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Shared View-Only (Tier 2)
sparkyfitness-db | SELECT create_checkin_policy('fasting_logs');
sparkyfitness-db | SELECT create_diary_policy('user_meal_visibilities');
sparkyfitness-db | SELECT create_checkin_policy('sleep_need_calculations');
sparkyfitness-db | SELECT create_checkin_policy('daily_sleep_need');
sparkyfitness-db | -- Day classification is a sleep/wellness (check-in) feature, used only by the
sparkyfitness-db | -- sleep-science service. It must use the check-in policy so check-in delegates
sparkyfitness-db | -- can manage it; the previous diary policy was a misclassification.
sparkyfitness-db | SELECT create_checkin_policy('day_classification_cache');
sparkyfitness-db | -- Onboarding data/status: delegates can read (to avoid repeated onboarding prompts when viewing
sparkyfitness-db | -- another user's diary) but only the account owner can submit or reset their own onboarding.
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_data FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_data FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_status FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_status FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Medications & Symptoms (Tier 3 - Delegate Writable with medications permission)
sparkyfitness-db | SELECT create_medication_policy('medications');
sparkyfitness-db | SELECT create_medication_policy('medication_schedules');
sparkyfitness-db | SELECT create_medication_policy('medication_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_pens');
sparkyfitness-db | SELECT create_medication_policy('injection_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_titration_steps');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptoms');
sparkyfitness-db | SELECT create_medication_policy('symptom_entries');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptom_locations');
sparkyfitness-db |
sparkyfitness-db | -- Medications Display Preferences (Tier 2 - Owner-Only Write, Delegate Read)
sparkyfitness-db | CREATE POLICY select_policy ON public.user_medication_display_preferences FOR SELECT TO PUBLIC USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_medication_display_preferences FOR ALL TO PUBLIC USING (authenticated_user_id() = user_id) WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | 2026-11-03 03:00:22.746 CET [62] LOG: checkpoint complete: wrote 155 buffers (0.9%), wrote 1 SLRU buffers; 0 WAL file(s) added, 0 removed, 0 recycled; write=15.533 s, sync=0.021 s, total=15.571 s; sync files=23, longest=0.015 s, average=0.001 s; distance=1370 kB, estimate=10122 kB; lsn=0/28A83F8, redo lsn=0/27D4BC8
sparkyfitness-db | 2026-11-03 03:00:58.633 CET [800] ERROR: relation "public.user_custom_symptom_locations" does not exist
sparkyfitness-db | 2026-11-03 03:00:58.633 CET [800] CONTEXT: SQL statement "ALTER TABLE public.user_custom_symptom_locations ENABLE ROW LEVEL SECURITY;"
sparkyfitness-db | PL/pgSQL function inline_code_block line 86 at EXECUTE
sparkyfitness-db | 2026-11-03 03:00:58.633 CET [800] STATEMENT: -- File: rls_policies.sql
sparkyfitness-db | -- =============================================================================
sparkyfitness-db | -- THIS FILE IS THE SINGLE SOURCE OF TRUTH FOR ALL RLS POLICIES IN THE APPLICATION.
sparkyfitness-db | -- It is executed on every server startup after migrations to ensure a consistent security state.
sparkyfitness-db | -- This script is generated from the db_schema_backup.sql to ensure all custom policies are included.
sparkyfitness-db | -- =============================================================================
sparkyfitness-db |
sparkyfitness-db | -- Step 1: Purge all existing RLS policies from the public schema in a single operation.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | pol RECORD;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR pol IN SELECT * FROM pg_policies WHERE schemaname = 'public'
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'DROP POLICY IF EXISTS ' || quote_ident(pol.policyname) || ' ON ' || quote_ident(pol.schemaname) || '.' || quote_ident(pol.tablename);
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 2: Enable RLS on all relevant tables to ensure consistent security state.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | table_name text;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR table_name IN SELECT unnest(ARRAY[
sparkyfitness-db | 'ai_service_settings',
sparkyfitness-db | 'check_in_measurements',
sparkyfitness-db | 'check_in_photos',
sparkyfitness-db | 'custom_categories',
sparkyfitness-db | 'custom_measurements',
sparkyfitness-db | 'exercise_entries',
sparkyfitness-db | 'exercise_entry_sets',
sparkyfitness-db | 'exercise_entry_activity_details',
sparkyfitness-db | 'exercises',
sparkyfitness-db | 'exercise_preset_entries',
sparkyfitness-db | 'external_data_providers',
sparkyfitness-db | 'family_access',
sparkyfitness-db | 'food_entries',
sparkyfitness-db | 'food_entry_meals',
sparkyfitness-db | 'food_variants',
sparkyfitness-db | 'foods',
sparkyfitness-db | 'goal_presets',
sparkyfitness-db | 'meal_foods',
sparkyfitness-db | 'meal_plan_template_assignments',
sparkyfitness-db | 'meal_plan_templates',
sparkyfitness-db | 'meal_plans',
sparkyfitness-db | 'meals',
sparkyfitness-db | 'meal_types',
sparkyfitness-db | 'mood_entries',
sparkyfitness-db | 'onboarding_data',
sparkyfitness-db | 'onboarding_status',
sparkyfitness-db | 'profiles',
sparkyfitness-db | 'sparky_chat_history',
sparkyfitness-db | 'admin_activity_logs',
sparkyfitness-db | 'api_key',
sparkyfitness-db | 'user_goals',
sparkyfitness-db | 'user_ignored_updates',
sparkyfitness-db | 'user_meal_visibilities',
sparkyfitness-db | 'user_nutrient_display_preferences',
sparkyfitness-db | 'user_oidc_links',
sparkyfitness-db | 'user_preferences',
sparkyfitness-db | 'user_water_containers',
sparkyfitness-db | 'water_intake',
sparkyfitness-db | 'water_intake_entries',
sparkyfitness-db | 'weekly_goal_plans',
sparkyfitness-db | 'workout_plan_assignment_sets',
sparkyfitness-db | 'workout_plan_template_assignments',
sparkyfitness-db | 'workout_plan_templates',
sparkyfitness-db | 'workout_preset_exercise_sets',
sparkyfitness-db | 'workout_preset_exercises',
sparkyfitness-db | 'workout_presets',
sparkyfitness-db | 'sleep_entries',
sparkyfitness-db | 'sleep_entry_stages',
sparkyfitness-db | 'fasting_logs',
sparkyfitness-db | 'user_custom_nutrients',
sparkyfitness-db | 'user_allergen_preferences',
sparkyfitness-db | 'user_dashboard_layouts',
sparkyfitness-db | 'sleep_need_calculations',
sparkyfitness-db | 'daily_sleep_need',
sparkyfitness-db | 'day_classification_cache',
sparkyfitness-db | 'medications',
sparkyfitness-db | 'medication_schedules',
sparkyfitness-db | 'medication_entries',
sparkyfitness-db | 'medication_pens',
sparkyfitness-db | 'injection_entries',
sparkyfitness-db | 'medication_titration_steps',
sparkyfitness-db | 'user_custom_symptoms',
sparkyfitness-db | 'symptom_entries',
sparkyfitness-db | 'user_medication_display_preferences',
sparkyfitness-db | 'user_custom_symptom_locations',
sparkyfitness-db | 'cycle_settings',
sparkyfitness-db | 'cycle_daily_entries',
sparkyfitness-db | 'cycles',
sparkyfitness-db | 'user_cycle_display_preferences',
sparkyfitness-db | 'cycle_test_entries',
sparkyfitness-db | 'pregnancies',
sparkyfitness-db | 'pregnancy_kick_sessions',
sparkyfitness-db | 'pregnancy_contractions',
sparkyfitness-db | 'pregnancy_photos',
sparkyfitness-db | 'pregnancy_checklist_state',
sparkyfitness-db | 'health_appointments',
sparkyfitness-db | 'user_custom_moods',
sparkyfitness-db | 'user_mood_display_preferences'
sparkyfitness-db | ]::text[])
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'ALTER TABLE public.' || quote_ident(table_name) || ' ENABLE ROW LEVEL SECURITY;';
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 3: Define reusable helper functions for common RLS conditions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION current_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION authenticated_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.authenticated_user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.get_accessible_users(p_user_id UUID)
sparkyfitness-db | RETURNS TABLE(
sparkyfitness-db | user_id UUID,
sparkyfitness-db | full_name TEXT,
sparkyfitness-db | email TEXT,
sparkyfitness-db | permissions JSONB,
sparkyfitness-db | access_end_date TIMESTAMP WITH TIME ZONE
sparkyfitness-db | ) AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | RETURN QUERY
sparkyfitness-db | SELECT
sparkyfitness-db | fa.owner_user_id,
sparkyfitness-db | p.full_name,
sparkyfitness-db | u.email::TEXT,
sparkyfitness-db | fa.access_permissions,
sparkyfitness-db | fa.access_end_date
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | JOIN public.profiles p ON p.id = fa.owner_user_id
sparkyfitness-db | JOIN public."user" u ON u.id = fa.owner_user_id
sparkyfitness-db | WHERE fa.family_user_id = p_user_id
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions);
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.find_user_by_email(p_email TEXT)
sparkyfitness-db | RETURNS UUID AS $func$
sparkyfitness-db | DECLARE
sparkyfitness-db | v_user_id UUID;
sparkyfitness-db | BEGIN
sparkyfitness-db | SELECT id INTO v_user_id
sparkyfitness-db | FROM public."user"
sparkyfitness-db | WHERE LOWER(email) = LOWER(p_email)
sparkyfitness-db | LIMIT 1;
sparkyfitness-db |
sparkyfitness-db | RETURN v_user_id;
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access(owner_uuid uuid, perm text) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions ->> perm)::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access_or(owner_uuid uuid, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_diary')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Centralized helper: returns true if the given permissions JSONB contains
sparkyfitness-db | -- at least one meaningful delegation permission. Used by has_profile_read_access
sparkyfitness-db | -- and get_accessible_users to avoid duplicating the permission key list.
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_any_meaningful_permission(perms jsonb) RETURNS bool
sparkyfitness-db | LANGUAGE sql IMMUTABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT (
sparkyfitness-db | (perms->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (perms->>'can_view_reports')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_profile_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | -- Owner always has access. Family delegates require at least one meaningful permission
sparkyfitness-db | -- (diary, checkin, medications, or reports) to read profile/layout/onboarding data.
sparkyfitness-db | -- A bare family_access row with no permissions does not grant read access.
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions)
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_checkin_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_read_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_medications')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_library_access_with_public(owner_uuid uuid, is_shared bool, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid
sparkyfitness-db | OR is_shared
sparkyfitness-db | OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true OR
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | AND (
sparkyfitness-db | p <> 'can_manage_diary'
sparkyfitness-db | OR current_user_id() = owner_uuid
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.set_app_context(UUID, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.set_app_context(p_user_id UUID, p_authenticated_user_id UUID)
sparkyfitness-db | RETURNS void AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- app.user_id is used by RLS to determine whose data is being accessed
sparkyfitness-db | PERFORM set_config('app.user_id', p_user_id::text, false);
sparkyfitness-db |
sparkyfitness-db | -- app.authenticated_user_id is the actual logged-in user
sparkyfitness-db | PERFORM set_config('app.authenticated_user_id', p_authenticated_user_id::text, false);
sparkyfitness-db | END;
sparkyfitness-db | $$ LANGUAGE plpgsql SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.can_access_user_data(UUID, TEXT, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.can_access_user_data(
sparkyfitness-db | target_user_id UUID,
sparkyfitness-db | permission_type TEXT,
sparkyfitness-db | auth_user_id UUID
sparkyfitness-db | ) RETURNS BOOLEAN AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Self access
sparkyfitness-db | IF target_user_id = auth_user_id THEN
sparkyfitness-db | RETURN TRUE;
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | -- Family access check
sparkyfitness-db | RETURN EXISTS (
sparkyfitness-db | SELECT 1
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | WHERE fa.family_user_id = auth_user_id
sparkyfitness-db | AND fa.owner_user_id = target_user_id
sparkyfitness-db | AND fa.is_active = TRUE
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > NOW())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>permission_type)::BOOLEAN = TRUE
sparkyfitness-db | OR
sparkyfitness-db | -- Mapping for common permission names
sparkyfitness-db | (permission_type = 'diary' AND (fa.access_permissions->>'can_manage_diary')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'checkin' AND (fa.access_permissions->>'can_manage_checkin')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'reports' AND (fa.access_permissions->>'can_view_reports')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | -- Inheritance: reports permission grants read access to others
sparkyfitness-db | (permission_type IN ('calorie', 'diary', 'mood', 'sleep', 'exercise', 'water', 'checkin')
sparkyfitness-db | AND (COALESCE((fa.access_permissions->>'reports')::BOOLEAN, FALSE)
sparkyfitness-db | OR COALESCE((fa.access_permissions->>'can_view_reports')::BOOLEAN, FALSE)))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | -- Helper function to check if authenticated user is admin
sparkyfitness-db | CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public."user" u
sparkyfitness-db | WHERE u.id = authenticated_user_id()
sparkyfitness-db | AND u.role = 'admin'
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Step 4: Define generic policy creation functions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY owner_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_shared_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (%I = current_user_id());
sparkyfitness-db | ', table_name, id_column);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_diary_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_checkin_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_checkin_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''))
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_library_policy(table_name text, shared_column text, permissions text[]) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | DECLARE
sparkyfitness-db | quoted_permissions text;
sparkyfitness-db | shared_expression text;
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Quote each permission name to ensure valid ARRAY syntax
sparkyfitness-db | SELECT array_to_string(ARRAY(
sparkyfitness-db | SELECT quote_literal(p) FROM unnest(permissions) p
sparkyfitness-db | ), ',') INTO quoted_permissions;
sparkyfitness-db |
sparkyfitness-db | -- Use boolean false if shared_column is 'false', otherwise treat as column name
sparkyfitness-db | IF shared_column = 'false' THEN
sparkyfitness-db | shared_expression := 'false';
sparkyfitness-db | ELSE
sparkyfitness-db | shared_expression := quote_ident(shared_column);
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_library_access_with_public(user_id, %s, ARRAY[%s]));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | ', table_name, shared_expression, quoted_permissions, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.create_medication_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_medication_access(user_id))
sparkyfitness-db | WITH CHECK (has_medication_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 5: Apply policies to all tables.
sparkyfitness-db | -- Custom policy for ai_service_settings to support admin-global + user-owned settings
sparkyfitness-db | -- Drop ALL possible old policy names before recreating
sparkyfitness-db | DROP POLICY IF EXISTS owner_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_insert_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_update_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_delete_policy ON public.ai_service_settings;
sparkyfitness-db | -- SELECT policy: All authenticated users can read public settings, users can read their own
sparkyfitness-db | CREATE POLICY ai_service_settings_select_policy ON public.ai_service_settings FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = TRUE AND authenticated_user_id() IS NOT NULL) OR
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id())
sparkyfitness-db | );
sparkyfitness-db | -- INSERT policy: Users can create their own settings, admins can create public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_insert_policy ON public.ai_service_settings FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- UPDATE policy: Users can update their own settings, admins can update public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_update_policy ON public.ai_service_settings FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- DELETE policy: Users can delete their own settings, admins can delete public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_delete_policy ON public.ai_service_settings FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Owner-only access tables
sparkyfitness-db | -- Tier 1: Strictly Private (no delegation allowed)
sparkyfitness-db | SELECT create_owner_policy('api_key', 'reference_id');
sparkyfitness-db | SELECT create_owner_policy('user_oidc_links');
sparkyfitness-db | SELECT create_owner_policy('sparky_chat_history');
sparkyfitness-db |
sparkyfitness-db | -- Profiles: delegates can read (with any meaningful permission) but only owner can write.
sparkyfitness-db | -- Delegates do not need to modify another user's profile to manage their diary.
sparkyfitness-db | CREATE POLICY select_policy ON public.profiles FOR SELECT TO PUBLIC USING (has_profile_read_access(id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.profiles FOR ALL TO PUBLIC USING (authenticated_user_id() = id) WITH CHECK (authenticated_user_id() = id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.user_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('user_goals');
sparkyfitness-db | SELECT create_diary_policy('weekly_goal_plans');
sparkyfitness-db | SELECT create_diary_policy('user_water_containers');
sparkyfitness-db | SELECT create_diary_policy('user_custom_nutrients');
sparkyfitness-db | SELECT create_diary_policy('user_allergen_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Nutrient display preferences: delegates can read but only owner can rearrange their own columns.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_nutrient_display_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_nutrient_display_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Dashboard layouts: delegates can read but only owner can rearrange their own dashboard.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_dashboard_layouts FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_dashboard_layouts FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | SELECT create_diary_policy('goal_presets');
sparkyfitness-db | SELECT create_diary_policy('meal_plans');
sparkyfitness-db | SELECT create_checkin_policy('mood_entries');
sparkyfitness-db |
sparkyfitness-db | -- Admin Activity Logs: Only the admin who performed the action or other admins can view
sparkyfitness-db | CREATE POLICY admin_only_select ON public.admin_activity_logs FOR SELECT TO PUBLIC
sparkyfitness-db | USING (admin_user_id = current_user_id() OR is_admin());
sparkyfitness-db | CREATE POLICY admin_only_insert ON public.admin_activity_logs FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (admin_user_id = current_user_id() AND is_admin());
sparkyfitness-db |
sparkyfitness-db | -- Diary access tables
sparkyfitness-db | SELECT create_checkin_policy('check_in_measurements');
sparkyfitness-db | SELECT create_checkin_policy('check_in_photos');
sparkyfitness-db | -- Custom categories/measurements are surfaced through the check-in feature and
sparkyfitness-db | -- every /measurements route is guarded by checkPermissionMiddleware('checkin'),
sparkyfitness-db | -- so their RLS must use the check-in policy. The previous diary policy required
sparkyfitness-db | -- can_manage_diary to write, which blocked check-in delegates (e.g. the GLP-1
sparkyfitness-db | -- daily check-in) from saving even though the route allowed them.
sparkyfitness-db | SELECT create_checkin_policy('custom_categories');
sparkyfitness-db | SELECT create_checkin_policy('custom_measurements');
sparkyfitness-db | SELECT create_diary_policy('exercise_entries');
sparkyfitness-db | -- Custom policy for exercise_entries to allow access if linked to an owned exercise_preset_entry
sparkyfitness-db | CREATE POLICY select_exercise_preset_entry_linked_policy ON public.exercise_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | exercise_preset_entry_id IS NOT NULL AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.exercise_preset_entries epe
sparkyfitness-db | WHERE epe.id = exercise_entries.exercise_preset_entry_id AND has_diary_read_access(epe.user_id)
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- The modify policy for exercise_entries is already handled by create_diary_policy('exercise_entries')
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('exercise_preset_entries');
sparkyfitness-db | SELECT create_diary_policy('food_entry_meals');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entries');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entry_stages');
sparkyfitness-db | SELECT create_diary_policy('water_intake');
sparkyfitness-db | SELECT create_diary_policy('water_intake_entries');
sparkyfitness-db |
sparkyfitness-db | -- Library access tables
sparkyfitness-db | SELECT create_library_policy('exercises', 'shared_with_public', ARRAY['can_view_exercise_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('foods', 'shared_with_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meals', 'is_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meal_plan_templates', 'false', ARRAY['can_view_food_library']);
sparkyfitness-db | SELECT create_library_policy('workout_plan_templates', 'false', ARRAY['can_view_exercise_library']);
sparkyfitness-db | SELECT create_library_policy('workout_presets', 'is_public', ARRAY['can_view_exercise_library','can_manage_diary']);
sparkyfitness-db |
sparkyfitness-db | -- Medication & GLP-1 tracker (see migration 20260624000000_add_medication_glp1_schema.sql).
sparkyfitness-db | -- These tables are managed by create_medication_policy at the bottom of this file (Tier 3).
sparkyfitness-db | -- Do NOT apply create_library_policy or create_diary_policy to medication tables.
sparkyfitness-db | SELECT create_owner_policy('user_medication_display_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Cycle & Pregnancy hub (see migration 20260702180000_add_cycle_tracking_schema.sql).
sparkyfitness-db | -- Tier 1 — owner-only. Deliberately stricter than medications: this reproductive
sparkyfitness-db | -- health data is NEVER shared or delegated in v1 (no family/caregiver access).
sparkyfitness-db | SELECT create_owner_policy('cycle_settings');
sparkyfitness-db | SELECT create_owner_policy('cycle_daily_entries');
sparkyfitness-db | SELECT create_owner_policy('cycles');
sparkyfitness-db | SELECT create_owner_policy('user_cycle_display_preferences');
sparkyfitness-db | SELECT create_owner_policy('cycle_test_entries');
sparkyfitness-db |
sparkyfitness-db | -- Pregnancy mode (see migration 20260702200000_add_pregnancy_schema.sql). Tier 1
sparkyfitness-db | -- owner-only. health_appointments is generic but still owner-only in v1.
sparkyfitness-db | SELECT create_owner_policy('pregnancies');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_kick_sessions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_contractions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_photos');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_checklist_state');
sparkyfitness-db | SELECT create_owner_policy('health_appointments');
sparkyfitness-db |
sparkyfitness-db | -- User-defined mood tags. Mood is check-in data (mood_entries uses the check-in
sparkyfitness-db | -- policy), and custom check-in definitions like custom_categories are shared with
sparkyfitness-db | -- check-in delegates — so custom moods follow the same check-in policy for
sparkyfitness-db | -- consistency (a delegate managing the owner's check-in sees the owner's moods).
sparkyfitness-db | SELECT create_checkin_policy('user_custom_moods');
sparkyfitness-db |
sparkyfitness-db | -- Mood display preferences: personal picker config, owner-only.
sparkyfitness-db | SELECT create_owner_policy('user_mood_display_preferences');
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | -- Custom policies for special cases
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_read_access(ee.user_id)));
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)));
sparkyfitness-db |
sparkyfitness-db | -- Provider configs: admin-global (is_public) OR own OR family delegation
sparkyfitness-db | -- Drop any old policy names first (idempotent)
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS insert_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS update_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS delete_policy ON public.external_data_providers;
sparkyfitness-db |
sparkyfitness-db | -- SELECT: admin-global (authenticated users), own, or explicit family delegation
sparkyfitness-db | CREATE POLICY select_policy ON public.external_data_providers FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | -- Admin-created global providers: all authenticated users can see them if active
sparkyfitness-db | (is_public = TRUE AND is_active = TRUE AND authenticated_user_id() IS NOT NULL)
sparkyfitness-db | -- Owner always sees their own providers
sparkyfitness-db | OR (is_public = FALSE AND current_user_id() = user_id)
sparkyfitness-db | OR (
sparkyfitness-db | -- Explicit family delegation: share_external_providers permission, non-strictly-private only, must be active
sparkyfitness-db | is_public = FALSE AND is_active = TRUE AND has_family_access(user_id, 'share_external_providers') AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.external_provider_types ept
sparkyfitness-db | WHERE ept.id = external_data_providers.provider_type
sparkyfitness-db | AND ept.is_strictly_private = FALSE
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- INSERT: users create their own (is_public=FALSE), admins create global (is_public=TRUE)
sparkyfitness-db | CREATE POLICY insert_policy ON public.external_data_providers FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- UPDATE: users update own, admins update global
sparkyfitness-db | CREATE POLICY update_policy ON public.external_data_providers FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- DELETE: users delete own, admins delete global
sparkyfitness-db | CREATE POLICY delete_policy ON public.external_data_providers FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.family_access FOR SELECT TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id OR authenticated_user_id() = family_user_id);
sparkyfitness-db | CREATE POLICY insert_policy ON public.family_access FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db | CREATE POLICY modify_policy ON public.family_access FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY insert_policy ON public.food_entries FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | has_diary_access(user_id) AND (
sparkyfitness-db | (food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = food_entries.food_id)) OR
sparkyfitness-db | (meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = food_entries.meal_id))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY update_policy ON public.food_entries FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | CREATE POLICY delete_policy ON public.food_entries FOR DELETE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_variants FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND has_library_access_with_public(f.user_id, f.shared_with_public, ARRAY['can_view_food_library', 'can_manage_diary'])
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- Food variants are library data: only the owner of the parent food may write
sparkyfitness-db | -- them. Delegates (even can_manage_diary) get read-only access via select_policy
sparkyfitness-db | -- so they can pick serving sizes while logging, but cannot mutate the library.
sparkyfitness-db | CREATE POLICY modify_policy ON public.food_variants FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- meal_foods is polymorphic: a row references either a food (item_type='food')
sparkyfitness-db | -- or another meal (item_type='meal', a reusable sub-meal). Read access follows
sparkyfitness-db | -- the parent meal. Write access requires the caller owns the parent meal AND the
sparkyfitness-db | -- referenced ingredient is accessible to them: a food they can view, or a child
sparkyfitness-db | -- meal they have library access to (prevents linking a meal you cannot see).
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_foods FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND has_library_access_with_public(m.user_id, m.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_foods FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | AND (
sparkyfitness-db | (meal_foods.food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_foods.food_id))
sparkyfitness-db | OR
sparkyfitness-db | (meal_foods.child_meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals cm WHERE cm.id = meal_foods.child_meal_id AND has_library_access_with_public(cm.user_id, cm.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.meal_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_assignment_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercise_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_preset_exercises wpe WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercise_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercises FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_presets wp WHERE wp.id = workout_preset_exercises.workout_preset_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercises FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | -- Strictly Private (Tier 1)
sparkyfitness-db | SELECT create_owner_policy('user_ignored_updates');
sparkyfitness-db |
sparkyfitness-db | -- Meal types: access if user_id is null (system) or if user has diary access
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_types FOR SELECT TO PUBLIC
sparkyfitness-db | USING (user_id IS NULL OR has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_types FOR ALL TO PUBLIC
sparkyfitness-db | USING (user_id = authenticated_user_id())
sparkyfitness-db | WITH CHECK (user_id = authenticated_user_id());
sparkyfitness-db |
sparkyfitness-db | -- Activity details: access if linked exercise entry or preset entry is accessible
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_activity_details FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_read_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_read_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_activity_details FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Shared View-Only (Tier 2)
sparkyfitness-db | SELECT create_checkin_policy('fasting_logs');
sparkyfitness-db | SELECT create_diary_policy('user_meal_visibilities');
sparkyfitness-db | SELECT create_checkin_policy('sleep_need_calculations');
sparkyfitness-db | SELECT create_checkin_policy('daily_sleep_need');
sparkyfitness-db | -- Day classification is a sleep/wellness (check-in) feature, used only by the
sparkyfitness-db | -- sleep-science service. It must use the check-in policy so check-in delegates
sparkyfitness-db | -- can manage it; the previous diary policy was a misclassification.
sparkyfitness-db | SELECT create_checkin_policy('day_classification_cache');
sparkyfitness-db | -- Onboarding data/status: delegates can read (to avoid repeated onboarding prompts when viewing
sparkyfitness-db | -- another user's diary) but only the account owner can submit or reset their own onboarding.
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_data FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_data FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_status FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_status FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Medications & Symptoms (Tier 3 - Delegate Writable with medications permission)
sparkyfitness-db | SELECT create_medication_policy('medications');
sparkyfitness-db | SELECT create_medication_policy('medication_schedules');
sparkyfitness-db | SELECT create_medication_policy('medication_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_pens');
sparkyfitness-db | SELECT create_medication_policy('injection_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_titration_steps');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptoms');
sparkyfitness-db | SELECT create_medication_policy('symptom_entries');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptom_locations');
sparkyfitness-db |
sparkyfitness-db | -- Medications Display Preferences (Tier 2 - Owner-Only Write, Delegate Read)
sparkyfitness-db | CREATE POLICY select_policy ON public.user_medication_display_preferences FOR SELECT TO PUBLIC USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_medication_display_preferences FOR ALL TO PUBLIC USING (authenticated_user_id() = user_id) WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | 2026-11-03 03:01:39.844 CET [832] ERROR: relation "public.user_custom_symptom_locations" does not exist
sparkyfitness-db | 2026-11-03 03:01:39.844 CET [832] CONTEXT: SQL statement "ALTER TABLE public.user_custom_symptom_locations ENABLE ROW LEVEL SECURITY;"
sparkyfitness-db | PL/pgSQL function inline_code_block line 86 at EXECUTE
sparkyfitness-db | 2026-11-03 03:01:39.844 CET [832] STATEMENT: -- File: rls_policies.sql
sparkyfitness-db | -- =============================================================================
sparkyfitness-db | -- THIS FILE IS THE SINGLE SOURCE OF TRUTH FOR ALL RLS POLICIES IN THE APPLICATION.
sparkyfitness-db | -- It is executed on every server startup after migrations to ensure a consistent security state.
sparkyfitness-db | -- This script is generated from the db_schema_backup.sql to ensure all custom policies are included.
sparkyfitness-db | -- =============================================================================
sparkyfitness-db |
sparkyfitness-db | -- Step 1: Purge all existing RLS policies from the public schema in a single operation.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | pol RECORD;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR pol IN SELECT * FROM pg_policies WHERE schemaname = 'public'
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'DROP POLICY IF EXISTS ' || quote_ident(pol.policyname) || ' ON ' || quote_ident(pol.schemaname) || '.' || quote_ident(pol.tablename);
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 2: Enable RLS on all relevant tables to ensure consistent security state.
sparkyfitness-db | DO $$
sparkyfitness-db | DECLARE
sparkyfitness-db | table_name text;
sparkyfitness-db | BEGIN
sparkyfitness-db | FOR table_name IN SELECT unnest(ARRAY[
sparkyfitness-db | 'ai_service_settings',
sparkyfitness-db | 'check_in_measurements',
sparkyfitness-db | 'check_in_photos',
sparkyfitness-db | 'custom_categories',
sparkyfitness-db | 'custom_measurements',
sparkyfitness-db | 'exercise_entries',
sparkyfitness-db | 'exercise_entry_sets',
sparkyfitness-db | 'exercise_entry_activity_details',
sparkyfitness-db | 'exercises',
sparkyfitness-db | 'exercise_preset_entries',
sparkyfitness-db | 'external_data_providers',
sparkyfitness-db | 'family_access',
sparkyfitness-db | 'food_entries',
sparkyfitness-db | 'food_entry_meals',
sparkyfitness-db | 'food_variants',
sparkyfitness-db | 'foods',
sparkyfitness-db | 'goal_presets',
sparkyfitness-db | 'meal_foods',
sparkyfitness-db | 'meal_plan_template_assignments',
sparkyfitness-db | 'meal_plan_templates',
sparkyfitness-db | 'meal_plans',
sparkyfitness-db | 'meals',
sparkyfitness-db | 'meal_types',
sparkyfitness-db | 'mood_entries',
sparkyfitness-db | 'onboarding_data',
sparkyfitness-db | 'onboarding_status',
sparkyfitness-db | 'profiles',
sparkyfitness-db | 'sparky_chat_history',
sparkyfitness-db | 'admin_activity_logs',
sparkyfitness-db | 'api_key',
sparkyfitness-db | 'user_goals',
sparkyfitness-db | 'user_ignored_updates',
sparkyfitness-db | 'user_meal_visibilities',
sparkyfitness-db | 'user_nutrient_display_preferences',
sparkyfitness-db | 'user_oidc_links',
sparkyfitness-db | 'user_preferences',
sparkyfitness-db | 'user_water_containers',
sparkyfitness-db | 'water_intake',
sparkyfitness-db | 'water_intake_entries',
sparkyfitness-db | 'weekly_goal_plans',
sparkyfitness-db | 'workout_plan_assignment_sets',
sparkyfitness-db | 'workout_plan_template_assignments',
sparkyfitness-db | 'workout_plan_templates',
sparkyfitness-db | 'workout_preset_exercise_sets',
sparkyfitness-db | 'workout_preset_exercises',
sparkyfitness-db | 'workout_presets',
sparkyfitness-db | 'sleep_entries',
sparkyfitness-db | 'sleep_entry_stages',
sparkyfitness-db | 'fasting_logs',
sparkyfitness-db | 'user_custom_nutrients',
sparkyfitness-db | 'user_allergen_preferences',
sparkyfitness-db | 'user_dashboard_layouts',
sparkyfitness-db | 'sleep_need_calculations',
sparkyfitness-db | 'daily_sleep_need',
sparkyfitness-db | 'day_classification_cache',
sparkyfitness-db | 'medications',
sparkyfitness-db | 'medication_schedules',
sparkyfitness-db | 'medication_entries',
sparkyfitness-db | 'medication_pens',
sparkyfitness-db | 'injection_entries',
sparkyfitness-db | 'medication_titration_steps',
sparkyfitness-db | 'user_custom_symptoms',
sparkyfitness-db | 'symptom_entries',
sparkyfitness-db | 'user_medication_display_preferences',
sparkyfitness-db | 'user_custom_symptom_locations',
sparkyfitness-db | 'cycle_settings',
sparkyfitness-db | 'cycle_daily_entries',
sparkyfitness-db | 'cycles',
sparkyfitness-db | 'user_cycle_display_preferences',
sparkyfitness-db | 'cycle_test_entries',
sparkyfitness-db | 'pregnancies',
sparkyfitness-db | 'pregnancy_kick_sessions',
sparkyfitness-db | 'pregnancy_contractions',
sparkyfitness-db | 'pregnancy_photos',
sparkyfitness-db | 'pregnancy_checklist_state',
sparkyfitness-db | 'health_appointments',
sparkyfitness-db | 'user_custom_moods',
sparkyfitness-db | 'user_mood_display_preferences'
sparkyfitness-db | ]::text[])
sparkyfitness-db | LOOP
sparkyfitness-db | EXECUTE 'ALTER TABLE public.' || quote_ident(table_name) || ' ENABLE ROW LEVEL SECURITY;';
sparkyfitness-db | END LOOP;
sparkyfitness-db | END $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 3: Define reusable helper functions for common RLS conditions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION current_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION authenticated_user_id() RETURNS uuid
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT NULLIF(current_setting('app.authenticated_user_id', true), '')::uuid;
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.get_accessible_users(p_user_id UUID)
sparkyfitness-db | RETURNS TABLE(
sparkyfitness-db | user_id UUID,
sparkyfitness-db | full_name TEXT,
sparkyfitness-db | email TEXT,
sparkyfitness-db | permissions JSONB,
sparkyfitness-db | access_end_date TIMESTAMP WITH TIME ZONE
sparkyfitness-db | ) AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | RETURN QUERY
sparkyfitness-db | SELECT
sparkyfitness-db | fa.owner_user_id,
sparkyfitness-db | p.full_name,
sparkyfitness-db | u.email::TEXT,
sparkyfitness-db | fa.access_permissions,
sparkyfitness-db | fa.access_end_date
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | JOIN public.profiles p ON p.id = fa.owner_user_id
sparkyfitness-db | JOIN public."user" u ON u.id = fa.owner_user_id
sparkyfitness-db | WHERE fa.family_user_id = p_user_id
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions);
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.find_user_by_email(p_email TEXT)
sparkyfitness-db | RETURNS UUID AS $func$
sparkyfitness-db | DECLARE
sparkyfitness-db | v_user_id UUID;
sparkyfitness-db | BEGIN
sparkyfitness-db | SELECT id INTO v_user_id
sparkyfitness-db | FROM public."user"
sparkyfitness-db | WHERE LOWER(email) = LOWER(p_email)
sparkyfitness-db | LIMIT 1;
sparkyfitness-db |
sparkyfitness-db | RETURN v_user_id;
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access(owner_uuid uuid, perm text) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions ->> perm)::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_family_access_or(owner_uuid uuid, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_diary')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Centralized helper: returns true if the given permissions JSONB contains
sparkyfitness-db | -- at least one meaningful delegation permission. Used by has_profile_read_access
sparkyfitness-db | -- and get_accessible_users to avoid duplicating the permission key list.
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_any_meaningful_permission(perms jsonb) RETURNS bool
sparkyfitness-db | LANGUAGE sql IMMUTABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT (
sparkyfitness-db | (perms->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (perms->>'can_view_reports')::boolean = true OR
sparkyfitness-db | (perms->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_profile_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | -- Owner always has access. Family delegates require at least one meaningful permission
sparkyfitness-db | -- (diary, checkin, medications, or reports) to read profile/layout/onboarding data.
sparkyfitness-db | -- A bare family_access row with no permissions does not grant read access.
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND has_any_meaningful_permission(fa.access_permissions)
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_diary_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_diary')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_checkin_read_access(owner_uuid uuid) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_checkin')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (fa.access_permissions->>'can_manage_medications')::boolean = true
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.has_medication_read_access(owner_uuid uuid) RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_manage_medications')::boolean = true OR
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION has_library_access_with_public(owner_uuid uuid, is_shared bool, perms text[]) RETURNS bool
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT authenticated_user_id() = owner_uuid
sparkyfitness-db | OR is_shared
sparkyfitness-db | OR EXISTS (
sparkyfitness-db | SELECT 1 FROM public.family_access fa
sparkyfitness-db | WHERE fa.owner_user_id = owner_uuid
sparkyfitness-db | AND fa.family_user_id = authenticated_user_id()
sparkyfitness-db | AND fa.is_active = true
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > now())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>'can_view_reports')::boolean = true OR
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM unnest(perms) p
sparkyfitness-db | WHERE (fa.access_permissions ->> p)::boolean = true
sparkyfitness-db | AND (
sparkyfitness-db | p <> 'can_manage_diary'
sparkyfitness-db | OR current_user_id() = owner_uuid
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.set_app_context(UUID, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.set_app_context(p_user_id UUID, p_authenticated_user_id UUID)
sparkyfitness-db | RETURNS void AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- app.user_id is used by RLS to determine whose data is being accessed
sparkyfitness-db | PERFORM set_config('app.user_id', p_user_id::text, false);
sparkyfitness-db |
sparkyfitness-db | -- app.authenticated_user_id is the actual logged-in user
sparkyfitness-db | PERFORM set_config('app.authenticated_user_id', p_authenticated_user_id::text, false);
sparkyfitness-db | END;
sparkyfitness-db | $$ LANGUAGE plpgsql SECURITY DEFINER;
sparkyfitness-db |
sparkyfitness-db | DROP FUNCTION IF EXISTS public.can_access_user_data(UUID, TEXT, UUID);
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.can_access_user_data(
sparkyfitness-db | target_user_id UUID,
sparkyfitness-db | permission_type TEXT,
sparkyfitness-db | auth_user_id UUID
sparkyfitness-db | ) RETURNS BOOLEAN AS $func$
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Self access
sparkyfitness-db | IF target_user_id = auth_user_id THEN
sparkyfitness-db | RETURN TRUE;
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | -- Family access check
sparkyfitness-db | RETURN EXISTS (
sparkyfitness-db | SELECT 1
sparkyfitness-db | FROM public.family_access fa
sparkyfitness-db | WHERE fa.family_user_id = auth_user_id
sparkyfitness-db | AND fa.owner_user_id = target_user_id
sparkyfitness-db | AND fa.is_active = TRUE
sparkyfitness-db | AND (fa.access_end_date IS NULL OR fa.access_end_date > NOW())
sparkyfitness-db | AND (
sparkyfitness-db | (fa.access_permissions->>permission_type)::BOOLEAN = TRUE
sparkyfitness-db | OR
sparkyfitness-db | -- Mapping for common permission names
sparkyfitness-db | (permission_type = 'diary' AND (fa.access_permissions->>'can_manage_diary')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'checkin' AND (fa.access_permissions->>'can_manage_checkin')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | (permission_type = 'reports' AND (fa.access_permissions->>'can_view_reports')::BOOLEAN = TRUE)
sparkyfitness-db | OR
sparkyfitness-db | -- Inheritance: reports permission grants read access to others
sparkyfitness-db | (permission_type IN ('calorie', 'diary', 'mood', 'sleep', 'exercise', 'water', 'checkin')
sparkyfitness-db | AND (COALESCE((fa.access_permissions->>'reports')::BOOLEAN, FALSE)
sparkyfitness-db | OR COALESCE((fa.access_permissions->>'can_view_reports')::BOOLEAN, FALSE)))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | END;
sparkyfitness-db | $func$ LANGUAGE plpgsql STABLE;
sparkyfitness-db |
sparkyfitness-db | -- Helper function to check if authenticated user is admin
sparkyfitness-db | CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean
sparkyfitness-db | LANGUAGE sql STABLE
sparkyfitness-db | AS $function$
sparkyfitness-db | SELECT EXISTS (
sparkyfitness-db | SELECT 1 FROM public."user" u
sparkyfitness-db | WHERE u.id = authenticated_user_id()
sparkyfitness-db | AND u.role = 'admin'
sparkyfitness-db | );
sparkyfitness-db | $function$;
sparkyfitness-db |
sparkyfitness-db | -- Step 4: Define generic policy creation functions.
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY owner_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_shared_owner_policy(table_name text, id_column text DEFAULT 'user_id') RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS owner_modify_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (%I = current_user_id());
sparkyfitness-db | ', table_name, id_column);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (%I = authenticated_user_id())
sparkyfitness-db | WITH CHECK (%I = authenticated_user_id());
sparkyfitness-db | ', table_name, id_column, id_column);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_diary_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_checkin_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_checkin_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''))
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id OR has_family_access(user_id, ''can_manage_checkin''));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION create_library_policy(table_name text, shared_column text, permissions text[]) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $_$
sparkyfitness-db | DECLARE
sparkyfitness-db | quoted_permissions text;
sparkyfitness-db | shared_expression text;
sparkyfitness-db | BEGIN
sparkyfitness-db | -- Quote each permission name to ensure valid ARRAY syntax
sparkyfitness-db | SELECT array_to_string(ARRAY(
sparkyfitness-db | SELECT quote_literal(p) FROM unnest(permissions) p
sparkyfitness-db | ), ',') INTO quoted_permissions;
sparkyfitness-db |
sparkyfitness-db | -- Use boolean false if shared_column is 'false', otherwise treat as column name
sparkyfitness-db | IF shared_column = 'false' THEN
sparkyfitness-db | shared_expression := 'false';
sparkyfitness-db | ELSE
sparkyfitness-db | shared_expression := quote_ident(shared_column);
sparkyfitness-db | END IF;
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_library_access_with_public(user_id, %s, ARRAY[%s]));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | ', table_name, shared_expression, quoted_permissions, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $_$;
sparkyfitness-db |
sparkyfitness-db | CREATE OR REPLACE FUNCTION public.create_medication_policy(table_name text) RETURNS void
sparkyfitness-db | LANGUAGE plpgsql
sparkyfitness-db | AS $$
sparkyfitness-db | BEGIN
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS select_policy ON public.%I;', table_name);
sparkyfitness-db | EXECUTE format('DROP POLICY IF EXISTS modify_policy ON public.%I;', table_name);
sparkyfitness-db |
sparkyfitness-db | EXECUTE format('
sparkyfitness-db | CREATE POLICY select_policy ON public.%I FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.%I FOR ALL TO PUBLIC
sparkyfitness-db | USING (has_medication_access(user_id))
sparkyfitness-db | WITH CHECK (has_medication_access(user_id));
sparkyfitness-db | ', table_name, table_name);
sparkyfitness-db | END;
sparkyfitness-db | $$;
sparkyfitness-db |
sparkyfitness-db | -- Step 5: Apply policies to all tables.
sparkyfitness-db | -- Custom policy for ai_service_settings to support admin-global + user-owned settings
sparkyfitness-db | -- Drop ALL possible old policy names before recreating
sparkyfitness-db | DROP POLICY IF EXISTS owner_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_select_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_insert_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_update_policy ON public.ai_service_settings;
sparkyfitness-db | DROP POLICY IF EXISTS ai_service_settings_delete_policy ON public.ai_service_settings;
sparkyfitness-db | -- SELECT policy: All authenticated users can read public settings, users can read their own
sparkyfitness-db | CREATE POLICY ai_service_settings_select_policy ON public.ai_service_settings FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = TRUE AND authenticated_user_id() IS NOT NULL) OR
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id())
sparkyfitness-db | );
sparkyfitness-db | -- INSERT policy: Users can create their own settings, admins can create public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_insert_policy ON public.ai_service_settings FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- UPDATE policy: Users can update their own settings, admins can update public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_update_policy ON public.ai_service_settings FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db | -- DELETE policy: Users can delete their own settings, admins can delete public settings
sparkyfitness-db | CREATE POLICY ai_service_settings_delete_policy ON public.ai_service_settings FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Owner-only access tables
sparkyfitness-db | -- Tier 1: Strictly Private (no delegation allowed)
sparkyfitness-db | SELECT create_owner_policy('api_key', 'reference_id');
sparkyfitness-db | SELECT create_owner_policy('user_oidc_links');
sparkyfitness-db | SELECT create_owner_policy('sparky_chat_history');
sparkyfitness-db |
sparkyfitness-db | -- Profiles: delegates can read (with any meaningful permission) but only owner can write.
sparkyfitness-db | -- Delegates do not need to modify another user's profile to manage their diary.
sparkyfitness-db | CREATE POLICY select_policy ON public.profiles FOR SELECT TO PUBLIC USING (has_profile_read_access(id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.profiles FOR ALL TO PUBLIC USING (authenticated_user_id() = id) WITH CHECK (authenticated_user_id() = id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.user_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('user_goals');
sparkyfitness-db | SELECT create_diary_policy('weekly_goal_plans');
sparkyfitness-db | SELECT create_diary_policy('user_water_containers');
sparkyfitness-db | SELECT create_diary_policy('user_custom_nutrients');
sparkyfitness-db | SELECT create_diary_policy('user_allergen_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Nutrient display preferences: delegates can read but only owner can rearrange their own columns.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_nutrient_display_preferences FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_nutrient_display_preferences FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Dashboard layouts: delegates can read but only owner can rearrange their own dashboard.
sparkyfitness-db | CREATE POLICY select_policy ON public.user_dashboard_layouts FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_dashboard_layouts FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db | SELECT create_diary_policy('goal_presets');
sparkyfitness-db | SELECT create_diary_policy('meal_plans');
sparkyfitness-db | SELECT create_checkin_policy('mood_entries');
sparkyfitness-db |
sparkyfitness-db | -- Admin Activity Logs: Only the admin who performed the action or other admins can view
sparkyfitness-db | CREATE POLICY admin_only_select ON public.admin_activity_logs FOR SELECT TO PUBLIC
sparkyfitness-db | USING (admin_user_id = current_user_id() OR is_admin());
sparkyfitness-db | CREATE POLICY admin_only_insert ON public.admin_activity_logs FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (admin_user_id = current_user_id() AND is_admin());
sparkyfitness-db |
sparkyfitness-db | -- Diary access tables
sparkyfitness-db | SELECT create_checkin_policy('check_in_measurements');
sparkyfitness-db | SELECT create_checkin_policy('check_in_photos');
sparkyfitness-db | -- Custom categories/measurements are surfaced through the check-in feature and
sparkyfitness-db | -- every /measurements route is guarded by checkPermissionMiddleware('checkin'),
sparkyfitness-db | -- so their RLS must use the check-in policy. The previous diary policy required
sparkyfitness-db | -- can_manage_diary to write, which blocked check-in delegates (e.g. the GLP-1
sparkyfitness-db | -- daily check-in) from saving even though the route allowed them.
sparkyfitness-db | SELECT create_checkin_policy('custom_categories');
sparkyfitness-db | SELECT create_checkin_policy('custom_measurements');
sparkyfitness-db | SELECT create_diary_policy('exercise_entries');
sparkyfitness-db | -- Custom policy for exercise_entries to allow access if linked to an owned exercise_preset_entry
sparkyfitness-db | CREATE POLICY select_exercise_preset_entry_linked_policy ON public.exercise_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | exercise_preset_entry_id IS NOT NULL AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.exercise_preset_entries epe
sparkyfitness-db | WHERE epe.id = exercise_entries.exercise_preset_entry_id AND has_diary_read_access(epe.user_id)
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- The modify policy for exercise_entries is already handled by create_diary_policy('exercise_entries')
sparkyfitness-db |
sparkyfitness-db | SELECT create_diary_policy('exercise_preset_entries');
sparkyfitness-db | SELECT create_diary_policy('food_entry_meals');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entries');
sparkyfitness-db | SELECT create_checkin_policy('sleep_entry_stages');
sparkyfitness-db | SELECT create_diary_policy('water_intake');
sparkyfitness-db | SELECT create_diary_policy('water_intake_entries');
sparkyfitness-db |
sparkyfitness-db | -- Library access tables
sparkyfitness-db | SELECT create_library_policy('exercises', 'shared_with_public', ARRAY['can_view_exercise_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('foods', 'shared_with_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meals', 'is_public', ARRAY['can_view_food_library', 'can_manage_diary']);
sparkyfitness-db | SELECT create_library_policy('meal_plan_templates', 'false', ARRAY['can_view_food_library']);
sparkyfitness-db | SELECT create_library_policy('workout_plan_templates', 'false', ARRAY['can_view_exercise_library']);
sparkyfitness-db | SELECT create_library_policy('workout_presets', 'is_public', ARRAY['can_view_exercise_library','can_manage_diary']);
sparkyfitness-db |
sparkyfitness-db | -- Medication & GLP-1 tracker (see migration 20260624000000_add_medication_glp1_schema.sql).
sparkyfitness-db | -- These tables are managed by create_medication_policy at the bottom of this file (Tier 3).
sparkyfitness-db | -- Do NOT apply create_library_policy or create_diary_policy to medication tables.
sparkyfitness-db | SELECT create_owner_policy('user_medication_display_preferences');
sparkyfitness-db |
sparkyfitness-db | -- Cycle & Pregnancy hub (see migration 20260702180000_add_cycle_tracking_schema.sql).
sparkyfitness-db | -- Tier 1 — owner-only. Deliberately stricter than medications: this reproductive
sparkyfitness-db | -- health data is NEVER shared or delegated in v1 (no family/caregiver access).
sparkyfitness-db | SELECT create_owner_policy('cycle_settings');
sparkyfitness-db | SELECT create_owner_policy('cycle_daily_entries');
sparkyfitness-db | SELECT create_owner_policy('cycles');
sparkyfitness-db | SELECT create_owner_policy('user_cycle_display_preferences');
sparkyfitness-db | SELECT create_owner_policy('cycle_test_entries');
sparkyfitness-db |
sparkyfitness-db | -- Pregnancy mode (see migration 20260702200000_add_pregnancy_schema.sql). Tier 1
sparkyfitness-db | -- owner-only. health_appointments is generic but still owner-only in v1.
sparkyfitness-db | SELECT create_owner_policy('pregnancies');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_kick_sessions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_contractions');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_photos');
sparkyfitness-db | SELECT create_owner_policy('pregnancy_checklist_state');
sparkyfitness-db | SELECT create_owner_policy('health_appointments');
sparkyfitness-db |
sparkyfitness-db | -- User-defined mood tags. Mood is check-in data (mood_entries uses the check-in
sparkyfitness-db | -- policy), and custom check-in definitions like custom_categories are shared with
sparkyfitness-db | -- check-in delegates — so custom moods follow the same check-in policy for
sparkyfitness-db | -- consistency (a delegate managing the owner's check-in sees the owner's moods).
sparkyfitness-db | SELECT create_checkin_policy('user_custom_moods');
sparkyfitness-db |
sparkyfitness-db | -- Mood display preferences: personal picker config, owner-only.
sparkyfitness-db | SELECT create_owner_policy('user_mood_display_preferences');
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | -- Custom policies for special cases
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_read_access(ee.user_id)));
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_sets.exercise_entry_id AND has_diary_access(ee.user_id)));
sparkyfitness-db |
sparkyfitness-db | -- Provider configs: admin-global (is_public) OR own OR family delegation
sparkyfitness-db | -- Drop any old policy names first (idempotent)
sparkyfitness-db | DROP POLICY IF EXISTS select_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS modify_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS insert_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS update_policy ON public.external_data_providers;
sparkyfitness-db | DROP POLICY IF EXISTS delete_policy ON public.external_data_providers;
sparkyfitness-db |
sparkyfitness-db | -- SELECT: admin-global (authenticated users), own, or explicit family delegation
sparkyfitness-db | CREATE POLICY select_policy ON public.external_data_providers FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | -- Admin-created global providers: all authenticated users can see them if active
sparkyfitness-db | (is_public = TRUE AND is_active = TRUE AND authenticated_user_id() IS NOT NULL)
sparkyfitness-db | -- Owner always sees their own providers
sparkyfitness-db | OR (is_public = FALSE AND current_user_id() = user_id)
sparkyfitness-db | OR (
sparkyfitness-db | -- Explicit family delegation: share_external_providers permission, non-strictly-private only, must be active
sparkyfitness-db | is_public = FALSE AND is_active = TRUE AND has_family_access(user_id, 'share_external_providers') AND EXISTS (
sparkyfitness-db | SELECT 1 FROM public.external_provider_types ept
sparkyfitness-db | WHERE ept.id = external_data_providers.provider_type
sparkyfitness-db | AND ept.is_strictly_private = FALSE
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- INSERT: users create their own (is_public=FALSE), admins create global (is_public=TRUE)
sparkyfitness-db | CREATE POLICY insert_policy ON public.external_data_providers FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- UPDATE: users update own, admins update global
sparkyfitness-db | CREATE POLICY update_policy ON public.external_data_providers FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- DELETE: users delete own, admins delete global
sparkyfitness-db | CREATE POLICY delete_policy ON public.external_data_providers FOR DELETE TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (is_public = FALSE AND user_id = authenticated_user_id()) OR
sparkyfitness-db | (is_public = TRUE AND is_admin())
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.family_access FOR SELECT TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id OR authenticated_user_id() = family_user_id);
sparkyfitness-db | CREATE POLICY insert_policy ON public.family_access FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db | CREATE POLICY modify_policy ON public.family_access FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = owner_user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = owner_user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_entries FOR SELECT TO PUBLIC
sparkyfitness-db | USING (has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY insert_policy ON public.food_entries FOR INSERT TO PUBLIC
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | has_diary_access(user_id) AND (
sparkyfitness-db | (food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = food_entries.food_id)) OR
sparkyfitness-db | (meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = food_entries.meal_id))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY update_policy ON public.food_entries FOR UPDATE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id))
sparkyfitness-db | WITH CHECK (has_diary_access(user_id));
sparkyfitness-db | CREATE POLICY delete_policy ON public.food_entries FOR DELETE TO PUBLIC
sparkyfitness-db | USING (has_diary_access(user_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.food_variants FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND has_library_access_with_public(f.user_id, f.shared_with_public, ARRAY['can_view_food_library', 'can_manage_diary'])
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db | -- Food variants are library data: only the owner of the parent food may write
sparkyfitness-db | -- them. Delegates (even can_manage_diary) get read-only access via select_policy
sparkyfitness-db | -- so they can pick serving sizes while logging, but cannot mutate the library.
sparkyfitness-db | CREATE POLICY modify_policy ON public.food_variants FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (
sparkyfitness-db | SELECT 1 FROM public.foods f
sparkyfitness-db | WHERE f.id = food_variants.food_id
sparkyfitness-db | AND authenticated_user_id() = f.user_id
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- meal_foods is polymorphic: a row references either a food (item_type='food')
sparkyfitness-db | -- or another meal (item_type='meal', a reusable sub-meal). Read access follows
sparkyfitness-db | -- the parent meal. Write access requires the caller owns the parent meal AND the
sparkyfitness-db | -- referenced ingredient is accessible to them: a food they can view, or a child
sparkyfitness-db | -- meal they have library access to (prevents linking a meal you cannot see).
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_foods FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND has_library_access_with_public(m.user_id, m.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_foods FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_foods.meal_id AND authenticated_user_id() = m.user_id)
sparkyfitness-db | AND (
sparkyfitness-db | (meal_foods.food_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_foods.food_id))
sparkyfitness-db | OR
sparkyfitness-db | (meal_foods.child_meal_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.meals cm WHERE cm.id = meal_foods.child_meal_id AND has_library_access_with_public(cm.user_id, cm.is_public, ARRAY['can_view_food_library', 'can_manage_diary'])))
sparkyfitness-db | )
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.meal_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.meal_plan_templates mpt WHERE mpt.id = meal_plan_template_assignments.template_id AND has_diary_access(mpt.user_id)) AND
sparkyfitness-db | (((item_type = 'food') AND EXISTS (SELECT 1 FROM public.foods f WHERE f.id = meal_plan_template_assignments.food_id)) OR
sparkyfitness-db | ((item_type = 'meal') AND EXISTS (SELECT 1 FROM public.meals m WHERE m.id = meal_plan_template_assignments.meal_id))));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_assignment_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_template_assignments wpta WHERE wpta.id = workout_plan_assignment_sets.assignment_id));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY owner_policy ON public.workout_plan_template_assignments FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)))
sparkyfitness-db | WITH CHECK (EXISTS (SELECT 1 FROM public.workout_plan_templates wpt WHERE wpt.id = workout_plan_template_assignments.template_id AND has_diary_access(wpt.user_id)));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercise_sets FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_preset_exercises wpe WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercise_sets FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_preset_exercises wpe
sparkyfitness-db | JOIN public.workout_presets wp ON wp.id = wpe.workout_preset_id
sparkyfitness-db | WHERE wpe.id = workout_preset_exercise_sets.workout_preset_exercise_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.workout_preset_exercises FOR SELECT TO PUBLIC
sparkyfitness-db | USING (EXISTS (SELECT 1 FROM public.workout_presets wp WHERE wp.id = workout_preset_exercises.workout_preset_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.workout_preset_exercises FOR ALL TO PUBLIC
sparkyfitness-db | USING (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ))
sparkyfitness-db | WITH CHECK (EXISTS (
sparkyfitness-db | SELECT 1 FROM public.workout_presets wp
sparkyfitness-db | WHERE wp.id = workout_preset_exercises.workout_preset_id
sparkyfitness-db | AND authenticated_user_id() = wp.user_id
sparkyfitness-db | ));
sparkyfitness-db |
sparkyfitness-db | -- Strictly Private (Tier 1)
sparkyfitness-db | SELECT create_owner_policy('user_ignored_updates');
sparkyfitness-db |
sparkyfitness-db | -- Meal types: access if user_id is null (system) or if user has diary access
sparkyfitness-db | CREATE POLICY select_policy ON public.meal_types FOR SELECT TO PUBLIC
sparkyfitness-db | USING (user_id IS NULL OR has_diary_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.meal_types FOR ALL TO PUBLIC
sparkyfitness-db | USING (user_id = authenticated_user_id())
sparkyfitness-db | WITH CHECK (user_id = authenticated_user_id());
sparkyfitness-db |
sparkyfitness-db | -- Activity details: access if linked exercise entry or preset entry is accessible
sparkyfitness-db | CREATE POLICY select_policy ON public.exercise_entry_activity_details FOR SELECT TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_read_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_read_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db | CREATE POLICY modify_policy ON public.exercise_entry_activity_details FOR ALL TO PUBLIC
sparkyfitness-db | USING (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | )
sparkyfitness-db | WITH CHECK (
sparkyfitness-db | (exercise_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_entries ee WHERE ee.id = exercise_entry_id AND has_diary_access(ee.user_id))) OR
sparkyfitness-db | (exercise_preset_entry_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.exercise_preset_entries epe WHERE epe.id = exercise_preset_entry_id AND has_diary_access(epe.user_id)))
sparkyfitness-db | );
sparkyfitness-db |
sparkyfitness-db | -- Shared View-Only (Tier 2)
sparkyfitness-db | SELECT create_checkin_policy('fasting_logs');
sparkyfitness-db | SELECT create_diary_policy('user_meal_visibilities');
sparkyfitness-db | SELECT create_checkin_policy('sleep_need_calculations');
sparkyfitness-db | SELECT create_checkin_policy('daily_sleep_need');
sparkyfitness-db | -- Day classification is a sleep/wellness (check-in) feature, used only by the
sparkyfitness-db | -- sleep-science service. It must use the check-in policy so check-in delegates
sparkyfitness-db | -- can manage it; the previous diary policy was a misclassification.
sparkyfitness-db | SELECT create_checkin_policy('day_classification_cache');
sparkyfitness-db | -- Onboarding data/status: delegates can read (to avoid repeated onboarding prompts when viewing
sparkyfitness-db | -- another user's diary) but only the account owner can submit or reset their own onboarding.
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_data FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_data FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | CREATE POLICY select_policy ON public.onboarding_status FOR SELECT TO PUBLIC USING (has_profile_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.onboarding_status FOR ALL TO PUBLIC
sparkyfitness-db | USING (authenticated_user_id() = user_id)
sparkyfitness-db | WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |
sparkyfitness-db | -- Medications & Symptoms (Tier 3 - Delegate Writable with medications permission)
sparkyfitness-db | SELECT create_medication_policy('medications');
sparkyfitness-db | SELECT create_medication_policy('medication_schedules');
sparkyfitness-db | SELECT create_medication_policy('medication_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_pens');
sparkyfitness-db | SELECT create_medication_policy('injection_entries');
sparkyfitness-db | SELECT create_medication_policy('medication_titration_steps');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptoms');
sparkyfitness-db | SELECT create_medication_policy('symptom_entries');
sparkyfitness-db | SELECT create_medication_policy('user_custom_symptom_locations');
sparkyfitness-db |
sparkyfitness-db | -- Medications Display Preferences (Tier 2 - Owner-Only Write, Delegate Read)
sparkyfitness-db | CREATE POLICY select_policy ON public.user_medication_display_preferences FOR SELECT TO PUBLIC USING (has_medication_read_access(user_id));
sparkyfitness-db | CREATE POLICY modify_policy ON public.user_medication_display_preferences FOR ALL TO PUBLIC USING (authenticated_user_id() = user_id) WITH CHECK (authenticated_user_id() = user_id);
sparkyfitness-db |