Storage Policies
Configure time-series retention, rollups, presets, and per-user policy selection.
Default behavior
If you configure nothing, the built-in fallback is a single raw tier from age 0 to null.
That means:
- all points are stored as raw rows
- original granularity is preserved
- retention is effectively infinite
- no rollups are created
- nothing is deleted by policy
How policies are applied
Policies are evaluated in this order:
- matching rule inside the user's assigned preset, if the user has one
- exact
provider + seriesTypedefault rule seriesTypedefault ruleproviderdefault rule- global default rule
- built-in raw-forever fallback
The same policy logic is used for:
- manual syncs
- cron syncs
- Garmin webhook ingestion
- SDK/mobile push ingestion
Configure the policy
await wearables.replaceTimeSeriesPolicyConfiguration(ctx, {
defaultRules: [
{
tiers: [{ kind: "raw", fromAge: "0m", toAge: null }],
},
{
provider: "garmin",
seriesType: "heart_rate",
tiers: [
{ kind: "raw", fromAge: "0m", toAge: "24h" },
{ kind: "rollup", fromAge: "24h", toAge: "7d", bucket: "30m" },
{ kind: "rollup", fromAge: "7d", toAge: null, bucket: "3h" },
],
},
],
presets: [
{
key: "pro",
rules: [
{
provider: "garmin",
seriesType: "heart_rate",
tiers: [
{ kind: "raw", fromAge: "0m", toAge: "7d" },
{ kind: "rollup", fromAge: "7d", toAge: null, bucket: "1h" },
],
},
],
},
],
maintenance: {
enabled: true,
interval: "1h",
},
});This example keeps a raw-everything fallback for any provider or metric that does not match a more specific rule. It then adds a Garmin-specific heart-rate rule that keeps raw samples for 24h, compacts older data into 30m rollups until 7d, and keeps 3h rollups after that.
It also defines a pro preset. Users assigned to that preset get a more generous Garmin heart-rate policy: raw samples for 7d, then 1h rollups forever. The hourly maintenance loop is what keeps stored rows aligned with those tiers over time.
Assign a preset to a user
await wearables.setUserTimeSeriesPolicyPreset(ctx, {
userId: "user-123",
presetKey: "pro",
});In this example, assigning pro to user-123 gives only that user the longer Garmin heart-rate raw window. Other users continue using the default rules unless they are assigned a different preset.
Clear a preset assignment with:
await wearables.setUserTimeSeriesPolicyPreset(ctx, {
userId: "user-123",
presetKey: null,
});Configuration reference
Top-level fields
| Field | Type | Required | Meaning |
|---|---|---|---|
defaultRules | TimeSeriesPolicyRuleInput[] | Yes | Deployment-wide policy rules. |
presets | TimeSeriesPolicyPresetInput[] | No | Named rule sets that can be assigned per user. |
maintenance.enabled | boolean | No | Enables the internal maintenance loop. Defaults to true. |
maintenance.interval | string | number | No | Frequency for the maintenance loop. Accepts compact durations or milliseconds. |
Rule fields
| Field | Type | Required | Meaning |
|---|---|---|---|
provider | ProviderName | No | Limit the rule to one provider. Omit for all providers. |
seriesType | SeriesType | string | No | Limit the rule to one metric. Omit for all metrics in scope. |
tiers | TimeSeriesTierInput[] | Yes | Ordered, contiguous age tiers. |
Valid provider values
garminsuuntopolarwhoopstravaapplesamsunggoogle
provider is not Garmin-only. The policy resolver accepts any ProviderName, and the same storage-policy engine is used for every source that writes time-series rows.
In practice today, policies actively affect:
garminwhoopsuuntoapplesamsunggoogle
Rules targeting strava or polar are still valid, but they do not currently change stored time-series behavior because those integrations do not currently write dataPoints rows.
seriesType
Use any supported metric key from Series Types, for example:
heart_rateresting_heart_rateoxygen_saturationstepsweightrespiratory_rategarmin_stress_level
Tier fields
| Field | Type | Applies to | Meaning |
|---|---|---|---|
kind | "raw" | "rollup" | all tiers | Whether data is stored as original rows or as bucketed rollups. |
fromAge | string | number | all tiers | Lower age boundary. 0m means newest data. |
toAge | string | number | null | all tiers | Upper age boundary. null means open-ended. |
bucket | string | number | rollup tiers | Bucket size. Must normalize to a whole number of minutes. |
aggregations | ("avg" | "min" | "max" | "last" | "count")[] | rollup tiers | Which summary values to keep. Defaults to all five. |
Duration values
Duration fields accept:
- compact strings such as
500ms,30s,15m,24h,7d,2w - non-negative numbers interpreted as milliseconds
Validation rules
The implementation enforces:
- the first tier must start at age
0 - tiers must be contiguous without gaps or overlap
- open-ended tiers must be last
- only one raw tier is allowed per rule
- rollup buckets must be positive whole-minute durations
If data becomes older than the last tier, maintenance deletes it.
Aggregations
| Aggregation | Meaning |
|---|---|
avg | Arithmetic mean of values in the bucket |
min | Lowest value in the bucket |
max | Highest value in the bucket |
last | Most recent value in the bucket |
count | Number of samples in the bucket |
For rollup-backed reads:
- the response always includes
value valueis chosen from the configured aggregations with this priority:avg, thenlast, thenmax, thenmin, thencount
So if you configure:
aggregations: ["last"];then value for the rollup point will be the bucket's last value.
Maintenance behavior
Scheduled maintenance is internal to the component and enabled by default. After an upgrade, older scheduled jobs drain. The component ignores duplicate jobs and keeps one active maintenance schedule.
It is responsible for:
- compacting raw rows into rollups when they age out of a raw tier
- compacting finer rollups into coarser rollups when an older tier uses larger buckets
- deleting data older than the final tier
If you disable maintenance, new writes still go to the correct tier for their current age, but older already-stored data is no longer guaranteed to migrate or delete on schedule.
Useful patterns
Keep raw forever:
{
tiers: [{ kind: "raw", fromAge: "0m", toAge: null }],
}Keep raw 24h, then 30-minute rollups until day 7, then delete:
{
provider: "garmin",
seriesType: "heart_rate",
tiers: [
{ kind: "raw", fromAge: "0m", toAge: "24h" },
{ kind: "rollup", fromAge: "24h", toAge: "7d", bucket: "30m" },
],
}Store rollups only, never raw:
{
provider: "garmin",
seriesType: "oxygen_saturation",
tiers: [
{
kind: "rollup",
fromAge: "0m",
toAge: null,
bucket: "5m",
aggregations: ["avg", "last", "count"],
},
],
}