# yamllint disable rule:line-length rule:document-start rule:comments
# Vendored, unmodified below this header, from wger-project/docker at commit
# 77fad3d9fae7180ce1ac132f4d5ed4cc30a8c20e
# (2026-08-21), file services/config-powersync/sync_rules.yaml.
# These files describe the schema PowerSync replicates, so they move together
# with the wger/server image rather than being fetched independently.
# Note that changes to this file are not watched.
# The service needs to be restarted for changes to take effect.
#
# A column added server-side does not reach rows already in bucket storage:
# the queries below (SELECT * included) only run when a row is replicated, and
# an AddField emits no WAL. After such a migration either run a backfill that
# touches the rows, or bump the version line below so the changed file forces
# a reprocess
#
# Sync rules version: 1
# Every table referenced here must be part of the "powersync" publication, which
# lists its tables explicitly (see the server's core migration 0027). Reading from
# a table that was not synced before also needs
# `ALTER PUBLICATION powersync ADD TABLE
;`
# Warning: a user may have at most 1000 buckets, i.e. parameter-query results
# summed across all streams. This counts the *parameter* rows, not the data
# rows inside a bucket (a single bucket can hold any number of rows). For a
# stream with a `with:` CTE the count is the number of rows the CTE returns,
# so for `user_ingredients` below that is the number of distinct ingredients
# a user has ever referenced. Exceeding the limit is a hard error
# (PSYNC_S2305 "Too many parameter query results").
# See https://docs.powersync.com/sync/rules/parameter-queries
#
# Streams are split by update frequency (cold / medium / hot) so that
# bucket compaction can collapse the head of hot buckets without being
# blocked by long-lived rows from cold tables.
#
# For details, see the documentation:
# https://docs.powersync.com/sync/streams/overview
# https://docs.powersync.com/maintenance-ops/compacting-buckets
config:
edition: 3
streams:
# Global reference data, shared by all users, changes rarely enough
core:
auto_subscribe: true
queries:
- SELECT * FROM core_language
- SELECT * FROM core_license
- SELECT * FROM core_repetitionunit
- SELECT * FROM core_weightunit
- SELECT * FROM exercises_exercise
- SELECT * FROM exercises_translation
- SELECT * FROM exercises_alias
- SELECT * FROM exercises_exercisecomment
- SELECT * FROM exercises_muscle
- SELECT * FROM exercises_exercise_muscles
- SELECT * FROM exercises_exercise_muscles_secondary
- SELECT * FROM exercises_equipment
- SELECT * FROM exercises_exercise_equipment
- SELECT * FROM exercises_exercisecategory
- SELECT * FROM exercises_exerciseimage
- SELECT * FROM exercises_exercisevideo
# COLD, per-user data that almost never changes after creation.
user_profile:
auto_subscribe: true
queries:
- SELECT * FROM core_userprofile WHERE CAST(user_id AS TEXT) = auth.user_id()
- SELECT * FROM gallery_image WHERE CAST(user_id AS TEXT) = auth.user_id()
# COLD but potentially large, only the per-user *filter set* changes when the user logs new foods
user_ingredients:
auto_subscribe: true
with:
user_ingredients: |
SELECT DISTINCT nutrition_synced_ingredient.id
FROM nutrition_synced_ingredient
WHERE nutrition_synced_ingredient.id IN (
SELECT nutrition_logitem.ingredient_id FROM nutrition_logitem
JOIN nutrition_nutritionplan ON nutrition_logitem.plan_id = nutrition_nutritionplan.id
WHERE CAST(nutrition_nutritionplan.user_id AS TEXT) = auth.user_id())
OR nutrition_synced_ingredient.id IN (
SELECT nutrition_mealitem.ingredient_id FROM nutrition_mealitem
JOIN nutrition_meal ON nutrition_mealitem.meal_id = nutrition_meal.id
JOIN nutrition_nutritionplan ON nutrition_meal.plan_id = nutrition_nutritionplan.id
WHERE CAST(nutrition_nutritionplan.user_id AS TEXT) = auth.user_id())
queries:
- SELECT * FROM nutrition_synced_ingredient AS nutrition_ingredient WHERE id IN user_ingredients
- |
SELECT nutrition_image.* FROM nutrition_image
WHERE nutrition_image.ingredient_id IN user_ingredients
- |
SELECT nutrition_ingredientweightunit.* FROM nutrition_ingredientweightunit
WHERE nutrition_ingredientweightunit.ingredient_id IN user_ingredients
# MEDIUM. Edited e.g. when the user builds or edits their routine or nutrition plan,
# but not on every workout.
user_planning:
auto_subscribe: true
queries:
# Routines (templates excluded)
- SELECT * FROM manager_routine WHERE CAST(user_id AS TEXT) = auth.user_id() AND is_template = FALSE
# Measurement categories (the measurements themselves are HOT, see below)
- SELECT * FROM measurements_category WHERE CAST(user_id AS TEXT) = auth.user_id()
# Nutrition plan structure (not the log items)
- SELECT * FROM nutrition_nutritionplan WHERE CAST(user_id AS TEXT) = auth.user_id()
- |
SELECT nutrition_meal.*
FROM nutrition_meal
JOIN nutrition_nutritionplan
ON nutrition_meal.plan_id = nutrition_nutritionplan.id
WHERE CAST(nutrition_nutritionplan.user_id AS TEXT) = auth.user_id()
- |
SELECT nutrition_mealitem.*
FROM nutrition_mealitem
JOIN nutrition_meal
ON nutrition_mealitem.meal_id = nutrition_meal.id
JOIN nutrition_nutritionplan
ON nutrition_meal.plan_id = nutrition_nutritionplan.id
WHERE CAST(nutrition_nutritionplan.user_id AS TEXT) = auth.user_id()
# HOT. Generates one or more new rows per workout / meal. Compaction has the
# biggest impact here, so it must stay isolated from the other streams above
user_activity:
auto_subscribe: true
queries:
# Measurements, including body weight entries, so typically one or more new rows per day
- |
SELECT measurements_measurement.*
FROM measurements_measurement
INNER JOIN measurements_category
ON measurements_measurement.category_id = measurements_category.id
WHERE CAST(measurements_category.user_id AS TEXT) = auth.user_id()
# Workout sessions and per-set logs
- SELECT * FROM manager_workoutsession WHERE CAST(user_id AS TEXT) = auth.user_id()
- SELECT * FROM manager_workoutlog WHERE CAST(user_id AS TEXT) = auth.user_id()
# Nutrition log entries
- |
SELECT nutrition_logitem.*
FROM nutrition_logitem
JOIN nutrition_nutritionplan
ON nutrition_logitem.plan_id = nutrition_nutritionplan.id
WHERE CAST(nutrition_nutritionplan.user_id AS TEXT) = auth.user_id()