Skip to content

Database Security & RLS Tiers ​

Last updated: 2026-10-20

SparkyFitness uses PostgreSQL Row-Level Security (RLS) to enforce strict data isolation between accounts, caregivers, family members, and third-party integrations (API keys). This document covers the security design, context switching mechanism, and the three-tier database table classifications.


Identity & Context Functions ​

PostgreSQL session variables are initialized at the start of every pool client transaction (via the public.set_app_context(userId, authenticatedUserId) helper). RLS policies evaluate access using these functions:

  1. current_user_id(): Returns the ID of the active profile context being viewed (which changes during family delegation context switching; read from app.user_id).
  2. authenticated_user_id(): Returns the ID of the true authenticated logged-in actor (which never changes during context switching; read from app.authenticated_user_id).
  3. can_access_user_data(targetUserId, permissionType, authenticatedUserId): Checks whether the authenticated user holds the given logical permission over the target user's data (diary, checkin, medications, symptoms, reports, and read-only *_read variants). Domain helpers such as has_diary_read_access, has_checkin_read_access, has_medication_access, and has_symptom_access wrap it. Cycle and pregnancy are owner-only and are not delegatable.

The Three Security Tiers ​

All tables in the SparkyFitness database are classified into one of three security tiers to isolate data access according to the least-privilege principle:

Tier 1: Strictly Private (Owner-Only Read & Write) ​

These tables contain highly sensitive credentials, API keys, SSO tokens, 2FA recovery data, or private log histories. Only the owner (authenticated_user_id()) can view or modify these records. Family delegates and context-switched API keys have absolutely zero access.

Table NameDescriptionWrite (insert/update/delete)Read (select)
userAccount identity, password hash, role, email statusOwner-Only (authenticated_user_id())Owner-Only
two_factorTwo-factor authentication (2FA) recovery codes and secretsOwner-OnlyOwner-Only
verificationEmail verification tokensOwner-OnlyOwner-Only
api_keyUser-generated API keys for external accessOwner-OnlyOwner-Only
user_oidc_linksOpenID Connect SSO credentials linksOwner-OnlyOwner-Only
passkeyPasskey credentials for passwordless loginOwner-OnlyOwner-Only
sessionActive user authentication sessionsOwner-OnlyOwner-Only
passkey_registration_ticketsShort-lived (~60s), single-use tickets bridging the mobile→browser passkey registration handoff (transient session material)System-Only (RLS on, no policies; getSystemClient only)System-Only
accountAuth credentials and email accountsOwner-OnlyOwner-Only
sparky_chat_historyAI Assistant chat messages and historyOwner-OnlyOwner-Only
food_assistant_preferencesExplicit lasting food preferencesOwner-OnlyOwner-Only
food_assistant_tasksRecoverable food tasks, ingredient drafts and source evidenceOwner-OnlyOwner-Only
food_assistant_operationsIdempotent operations and before/after audit recordsOwner-OnlyOwner-Only
user_ignored_updatesRecords of skipped release updatesOwner-OnlyOwner-Only
admin_activity_logsAdmin action auditsAdmin-OnlyAdmin-Only
ai_service_settingsAI configurations and credentials, including optional reasoning effort (medium by default) and the per-model supports_audio_input opt-inOwner-OnlyOwner-Only (Public configs readable by all)
cycle_settingsCycle & pregnancy hub settings (mode, cycle parameters, birth control, conditions)Owner-OnlyOwner-Only
cycle_daily_entriesPer-day cycle logs (flow, period products, BBT, cervical mucus, moods, libido, notes)Owner-OnlyOwner-Only
cyclesDerived/manually-corrected period & cycle history recordsOwner-OnlyOwner-Only
user_cycle_display_preferencesPrivate preference on cycle dashboard tile/card visibilityOwner-OnlyOwner-Only
user_mood_display_preferencesPersonal show/hide config for the mood pickerOwner-OnlyOwner-Only
user_fasting_preferencesPersonal intermittent fasting targets, protocol, and auto-calculation configurationOwner-OnlyOwner-Only
cycle_test_entriesOvulation (OPK) and pregnancy test logsOwner-OnlyOwner-Only
pregnanciesPregnancy records (due date, status, linked prenatal medication)Owner-OnlyOwner-Only
pregnancy_kick_sessionsFetal kick-counter sessionsOwner-OnlyOwner-Only
pregnancy_contractionsContraction timer logsOwner-OnlyOwner-Only
pregnancy_photosBump photo journal (image bytes served only via the authenticated GET /api/v2/pregnancy/photos/file/{id}; excluded from the public /uploads static mount)Owner-OnlyOwner-Only
pregnancy_checklist_stateWeekly pregnancy checklist completion/custom itemsOwner-OnlyOwner-Only
run_programsA person's run program (Beginner 5K), their place in it, its Diary schedule, goal date, AI adjustments and how hard finished workouts feltOwner-OnlyOwner-Only
custom_run_programsA person's own saved run or walk plans (name, activity, workouts) that they can startOwner-OnlyOwner-Only
routesA person's saved routes (a simplified track of points, optional turn cues, distance and elevation) that they drew, imported from GPX or saved from a recorded activityOwner-OnlyOwner-Only
health_appointmentsPrenatal & other health appointmentsOwner-OnlyOwner-Only
openfoodfacts_product_read_rate_limitSingleton coordination lease and cooldown for Open Food Facts product reads, including manual previews; contains no user data or credentialsSystem services onlyDeny all
openfoodfacts_sync_queueDormant automatic Open Food Facts upload state and retained history; stores food/user identifiers and retry metadata, never provider credentials. The manual first release does not enqueue or process these rows.System worker and owning userOwner-Only
rate_limitSign-in rate limit counters shared by every server instance, keyed by client address and route; holds no account data or credentialsBetter Auth onlyDeny all

Tier 2: View-Only Shared (Owner-Only Write, Delegate-Read) ​

These tables contain user profiles, layouts, display settings, and custom databases (like the foods/exercises database libraries). Switched delegates can view this data to render the dashboard, but only the account owner is allowed to create or edit them.

Table NameDescriptionWrite (insert/update/delete)Read (select)
profilesUser full name, height, and display metricsOwner-Only (authenticated_user_id())Switched Delegate (current_user_id())
user_preferencesUnit, calculation-method, calorie safety-floor, chart-scale, and Chat/Vision/Voice configuration preferences (active_voice_ai_service_id is nullable; NULL means system recognition, never the Chat provider; voice_input_enabled defaults to true and controls microphone visibility on web/mobile without clearing the selection)Owner-Only (authenticated_user_id())Switched Delegate
user_nutrient_display_preferencesRearranged nutrient column preferencesOwner-OnlySwitched Delegate
user_medication_display_preferencesPrivate preference on GLP-1/Medication displayOwner-OnlyDelegate with can_manage_medications or can_view_reports
user_dashboard_layoutsRearranged dashboard widget positionsOwner-OnlySwitched Delegate
onboarding_dataInitial user onboarding metricsOwner-OnlySwitched Delegate
onboarding_statusUser onboarding statusOwner-OnlySwitched Delegate
meal_typesCustom breakfast, lunch, dinner, snack layoutsOwner-OnlySwitched Delegate with can_manage_diary or can_view_reports
foodsCustom food items created by the user, including a freeform markdown notes reference fieldOwner-OnlyDelegate with can_view_food_library, can_manage_diary, or can_view_reports
food_variantsServing options / sizes for custom foodsOwner-OnlyDelegate with can_view_food_library, can_manage_diary, or can_view_reports
mealsCustom meals created by the user, including a freeform markdown notes reference fieldOwner-OnlyDelegate with can_view_food_library, can_manage_diary, or can_view_reports
meal_foodsIngredients assigned inside a custom meal — either a food (food_id) or a linked reusable sub-meal (child_meal_id, item_type='meal'). Writing a linked-meal row requires the caller to also have library access to the referenced sub-meal, in addition to owning the parent meal.Owner-Only (and the referenced food/sub-meal must be accessible to the owner)Delegate with can_view_food_library, can_manage_diary, or can_view_reports
exercisesCustom exercises created by the userOwner-OnlyDelegate with can_view_exercise_library, can_manage_diary, or can_view_reports
workout_presetsCustom workouts/presets created by the userOwner-OnlyDelegate with can_view_exercise_library, can_manage_diary, or can_view_reports
workout_preset_exercisesExercises assigned inside a workout presetOwner-OnlyDelegate with can_view_exercise_library, can_manage_diary, or can_view_reports
workout_preset_exercise_setsReps/sets configured inside a preset exerciseOwner-OnlyDelegate with can_view_exercise_library, can_manage_diary, or can_view_reports
workout_plan_templatesTemplates for weekly and sequential workout schedulesOwner-OnlyDelegate with can_view_exercise_library, can_manage_diary, or can_view_reports
external_data_providersConfigured API integration credentials, including personal or global Open Food Facts accountsOwner-OnlyDelegate with share_external_providers for search only; contribution credentials are not shared. Manual contribution endpoints require the food owner to be the authenticated actor and require separate data/photo confirmation for the exact preview.
family_accessSharing rules & delegation credentialsOwner-OnlyOwner or Switched Delegate
user_goalsActive daily calorie/macro goalsOwner-OnlyDelegate with can_manage_diary or can_view_reports
user_nutrient_goal_preferencesPer-user minimum/maximum/target goal direction override per nutrientOwner-OnlyDelegate with can_manage_diary or can_view_reports
weekly_goal_plansScheduled goal plan adjustmentsOwner-OnlyDelegate with can_manage_diary or can_view_reports
user_water_containersConfigured container sizes (e.g. 500ml bottle), hydration factors, and optional links to food/variant/meal categoryOwner-OnlyDelegate with can_manage_diary or can_view_reports

Library deletes vs. diary history ​

exercise_entries and food_entries are snapshots, not pointers. Each row carries its own copy of the display and nutrition data, so it stays readable after the library row it was logged from is gone. exercise_id and food_id are nullable and ON DELETE SET NULL (20260912150000_preserve_data_on_user_and_library_deletes.sql), so deleting a library item blanks the link and keeps the entry.

Key rules that must be maintained:

  1. Past and today's logged entries survive library delete. Historical diary entries carry their own snapshot (exercise_name, category, modality, instructions, sets, images, or full food nutrition) so they remain readable. deleteExerciseAndDependencies (models/exercise.ts) and deleteFoodAndDependencies (models/food.ts) preserve completed diary logs. Only explicit delete_with_history (force delete) purges a user's logged entries (deleteExerciseEntriesForUser / deleteFoodEntriesForUser), scoped strictly to user_id.
  2. Templates cascade and future plan entries are cleaned up. workout_preset_exercises, workout_plan_template_assignments, meal_foods, meal_plans, meal_plan_template_assignments and food_favorites cascade from the library row. Additionally, future uncompleted diary entries scheduled via workout plans (entry_date >= today AND workout_plan_assignment_id IS NOT NULL) are removed on exercise library delete so stale future tasks are not left on the calendar. Empty parent preset entries are cleaned up when their child entries are deleted.
  3. Shared items fallback to hide. When otherUserReferences > 0, genuine deletion is refused because cascading would strip other users' templates; the item is hidden instead (is_quick_exercise / is_quick_food).

A related trap: food_entries.images stores paths into uploads/foods/<foodId>/. Removing that directory on a plain delete blanks the picture on every preserved entry, including other users'. It is only cleared on delete_with_history, where no reference can remain.

Tier 3: Delegate-Writable (Daily Logs & Schedules) ​

These tables contain daily diaries, logging entries, and scheduler items. Caregivers/delegates with active delegation permissions are granted full read and write access to log or modify data on behalf of the owner.

A. Diary Logs (Writable by delegates with can_manage_diary / Readable by can_view_reports or can_manage_diary) ​

The food_entries insert policy permits detached nutrition snapshots when food_id, meal_id, and variant_id are all null. This branch still requires has_diary_access(user_id); detaching library links does not grant access to another person's diary.

Table NameDescriptionWrite (insert/update/delete)Read (select)
food_entriesLogged meals/calories consumed for the day, plus an optional per-entry markdown notes field. Snapshot-backed: carries its own food_name, brand_name, serving size and full nutrition, and food_id is ON DELETE SET NULL — see Library deletes vs. diary history belowDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
food_entry_mealsMeal details associated with logged food entries, including snapshotted dish yield (entry_total_servings), plus an optional per-entry markdown notes fieldDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
food_favoritesStarred (favorite) foods/meals for quick re-loggingDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
exercise_entriesLogged exercises/sets completed for the day. Snapshot-backed: carries its own exercise_name, category, modality, muscles, equipment, instructions and images, and exercise_id is nullable / ON DELETE SET NULL — see Library deletes vs. diary history belowDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
shoesRunning-shoe inventory (name, brand, model, retirement mileage, default pair); entries link to a pair through exercise_entries.shoe_id (ON DELETE SET NULL)Delegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
exercise_preset_entriesLogged workout presets for the dayDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
exercise_entry_setsReps, weights, and sets completed for the logged exercisesDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
exercise_entry_activity_detailsHeart rate, distance, and activity details for workout logsDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
water_intakeTotal water logged for the dayDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
water_intake_entriesIndividual logged water cups/containers for the day, plus optional linked food entry references and hydration factorsDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
meal_plan_template_assignmentsActive meal templates scheduled to the calendarDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
workout_plan_template_assignmentsActive workout templates scheduled to the calendarDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
workout_plan_assignment_setsActive sets scheduled in workout plansDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
workout_feedbackHow a logged workout (or one exercise in it) felt: difficulty (too_easy / just_right / too_hard), a pain flag and optional pain_note; drives adaptive workout suggestions. Writes must reference the same user's session/exercise entryDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports

B. Medication Logs (Writable by delegates with can_manage_medications / Readable by can_view_reports or can_manage_medications) ​

Table NameDescriptionWrite (insert/update/delete)Read (select)
medicationsCustom medication inventory lists (e.g. insulin)Delegate with can_manage_medicationsDelegate with can_manage_medications or can_view_reports
medication_schedulesReminders/schedules to take medicationsDelegate with can_manage_medicationsDelegate with can_manage_medications or can_view_reports
medication_entriesLogs of medications takenDelegate with can_manage_medicationsDelegate with can_manage_medications or can_view_reports
medication_pensTrackers for medication delivery pensDelegate with can_manage_medicationsDelegate with can_manage_medications or can_view_reports
injection_entriesInjection logs (e.g., site of injection)Delegate with can_manage_medicationsDelegate with can_manage_medications or can_view_reports
medication_titration_stepsAutomated titration dosage plansDelegate with can_manage_medicationsDelegate with can_manage_medications or can_view_reports

B2. Symptom Tracking (Writable by delegates with can_manage_symptoms / Readable by can_manage_symptoms or can_view_reports) ​

Symptoms are their own domain (they used to share the medications permission). Policies use create_symptom_policy, backed by has_symptom_access / has_symptom_read_access.

Table NameDescriptionWrite (insert/update/delete)Read (select)
user_custom_symptomsThe user's symptom definitions: template, section overrides, custom fields, scaleDelegate with can_manage_symptomsDelegate with can_manage_symptoms or can_view_reports
user_symptom_optionsPick-list library (locations, qualities, associated symptoms, triggers, relief methods); replaces user_custom_symptom_locationsDelegate with can_manage_symptomsDelegate with can_manage_symptoms or can_view_reports
symptom_entriesQuick logs and episodes (start/end, locations, triggers, severity timeline). Rows with source = 'cycle' come from the menstrual-cycle hub and are owner-only: no delegate can read or write them, whatever they holdDelegate with can_manage_symptoms (never source = 'cycle')Delegate with can_manage_symptoms or can_view_reports (never source = 'cycle')
symptom_entry_treatmentsMedications and relief methods used for an entry, with effectivenessDelegate with can_manage_symptomsDelegate with can_manage_symptoms or can_view_reports
symptom_entry_photosPhotos attached to an entry. Files live under uploads/symptoms/, which is denied on the public static mount and served only by the authenticated GET /api/v2/symptoms/photos/file/{id} routeDelegate with can_manage_symptomsDelegate with can_manage_symptoms or can_view_reports
symptom_free_daysExplicit "no symptoms today" markers (a day with neither an entry nor a marker is unknown, not symptom-free)Delegate with can_manage_symptomsDelegate with can_manage_symptoms or can_view_reports

C. Check-In & Wellness Logs (Writable by delegates with can_manage_checkin / Readable by can_view_reports or can_manage_checkin) ​

Table NameDescriptionWrite (insert/update/delete)Read (select)
check_in_measurementsWeight, body composition, BMR, and circumference measurementsDelegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
check_in_photosProgress photos (image bytes served only via the authenticated GET /api/measurements/check-in-photos/file/{id}; excluded from the public /uploads static mount)Delegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
sleep_entriesSleep logs (bedtime, wake time)Delegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
sleep_entry_stagesSleep stage breakdowns (REM, Deep, Light)Delegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
sleep_need_calculationsAI sleep need calculationsDelegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
daily_sleep_needSleep goals calculated for the dayDelegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
fasting_logsLogs of fasting timelines (start/end fast)Delegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
mood_entriesLogged mood check-ins, several per day: optional mood, energy and focus ratings, tags and journal notes, with a timestamp and a source (manual, garmin or import)Delegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
mindfulness_sessionsLogged mindfulness, meditation, and breathwork sessionsDelegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
day_classification_cacheDaily summary caching logsDelegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
custom_categoriesUser-defined custom measurement categories (e.g. GLP-1 daily check-in metrics)Delegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
custom_measurementsLogged values for custom measurement categoriesDelegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
user_custom_moodsUser-defined mood tags (icon/color) for the mood pickerDelegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
exercise_entry_lapsLap split intervals for exercise entriesDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
exercise_entry_distance_seriesDistance against active time for an entry with no route (a treadmill run from the watch), for its pace chart (one row per entry; points holds [seconds, metres] rows as JSONB)Delegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
exercise_entry_gps_pointsGPS trackpoints & telemetry for exercise entries (one row per workout; points holds the whole track as JSONB)Delegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
exercise_entry_hr_zonesTime-in-heart-rate-zone splits for exercise entriesDelegate with can_manage_diaryDelegate with can_manage_diary or can_view_reports
health_metric_samplesIntraday wearable telemetry — heart rate, HRV, respiration, SpO2, stress, body battery, skin temperature (one JSONB-bucketed row per user/metric/day/provider)Delegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
vitals_entriesBlood pressure, blood glucose, and body temperature telemetryDelegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports
daily_health_metricsDaily automated wearable summary, scores, total calories, and the total-calorie source capture timeDelegate with can_manage_checkinDelegate with can_manage_checkin or can_view_reports

System & Global Reference Tables (Public Read, Admin Write) ​

These tables store global configuration settings, lookup values, and reference metadata. They do not contain user-specific data and do not have Row-Level Security enabled. All authenticated users can read them, but only administrators can update them.

Table NameDescriptionWrite (insert/update/delete)Read (select)
global_settingsApplication global feature flags and system configurationsAdmin-OnlyAuthenticated Users
backup_settingsAutomated database backup timing and storage credentialsAdmin-OnlyAdmin-Only
sso_providerActive Single Sign-On providers (Google, Apple, etc.)Admin-OnlyPublic (Login page)
oidc_providersOpenID Connect integration settings and metadataAdmin-OnlyPublic
external_provider_typesSearch provider configurations lookup (FatSecret, USDA, Canadian Nutrient File, Livsmedelsverket)Admin-OnlyAuthenticated Users
medication_typesMedication categories lookup (GLP-1, Insulin, ADHD, etc.)Admin-OnlyAuthenticated Users
medication_route_typesMedication administration route lookup (Subcutaneous, Oral)Admin-OnlyAuthenticated Users
medication_schedule_typesMedication scheduling frequencies lookup (Daily, Weekly)Admin-OnlyAuthenticated Users

Released under the GPL-3.0 License.