Something went wrong

Thank you for being patient! We're working hard on resolving the issue

BackendDeps host-seam pattern - Lona Docs Log in

BackendDeps host-seam pattern

Each provider declares a host-supplied trait that its row bodies call into for any work that touches host-shaped state.

The rule

<Provider>BackendDeps methods speak only:

  • Connector types: Cell, Owner, WriteOutput.
  • Primitives: &str, Uuid, serde_json::Value, Range<NaiveDate>, etc.

If the row body would need to query the host's database by row uuid, the right move is add a method to BackendDeps. The host implements the method against its real types and converts at the boundary.

Two patterns in the wild

Thin deps + provider drives sync (Garmin, Whoop)

pub trait GarminBackendDeps: Send + Sync {
  fn persist_cells<'a>(
    &'a self,
    row_id: Uuid,
    cells: Vec<Cell>,
  ) -> BoxFuture<'a, anyhow::Result<()>>;

  fn recent_log_cells<'a>(
    &'a self,
    owner: &'a Owner,
    lookup_key: &'a str,
  ) -> BoxFuture<'a, anyhow::Result<Vec<Cell>>>;
}

The Row body in tento-garmin/src/connector/sleep/mod.rs walks date ranges, calls the Garmin SDK, builds Vec<Cell>, and calls ctx.deps.persist_cells(...). Sync logic lives in the provider crate.

This is the default pattern. Use it when:

  • The sync logic is small enough to fit inside Row::sync.
  • The data lives entirely under one row.
  • The host's role is just "store these cells" + "give me the log list".

Rich deps + host drives sync (Google)

pub trait GoogleBackendDeps: Send + Sync {
  fn run_calendar_sync(&self, owner: &Owner, auth_id: &str, …) -> …;
  fn create_calendar_event(&self, …) -> …;
  fn update_calendar_event(&self, …) -> …;
  fn delete_calendar_event(&self, …) -> …;
  fn recent_log_cells(&self, …) -> …;
}

Google's Row::sync body just calls ctx.deps.run_calendar_sync(...). The actual sync logic lives host-side because the EventDb / sync-token / multi-calendar state machinery touches host data heavily and didn't decompose cleanly along Cell boundaries.

Use this pattern only when:

  • The row's logic spans multiple host-shaped tables (e.g. events + calendars + watches).
  • Transactional invariants cross the Cell abstraction.
  • The provider's API surface forces multi-step CRUD (token exchange, conditional writes, etc.) that maps poorly to a single persist_cells shape.

If you find yourself reaching for "rich deps" early, double-check whether you can split the sync into a Row::sync body that emits Vec<Cell> plus a host-side post-processor. The thin pattern is worth fighting for — it keeps the provider crate independently useful.

Naming

The trait is always <Provider>BackendDeps (singular Backend, plural Deps). Live examples:

  • tento-garmin/src/connector/mod.rs::GarminBackendDeps
  • tento-whoop/src/connector/mod.rs::WhoopBackendDeps
  • tento-google/src/connector/mod.rs::GoogleBackendDeps
  • tento-weather/src/connector/mod.rs::WeatherBackendDeps

Weather has a second trait WeatherLogsDeps for the :logs sub-row — fine to split when one row family needs different host seams than another.

Default impls

The trait should not provide default method implementations. If a seam has a sensible no-op default, that suggests the row doesn't need that seam at all — just don't call it. Forcing the host to write unimplemented!() is better than giving them a silent no-op.

How the host wires it

Each <Provider>BackendDeps is registered into the host's [IntegrationDeps] registry — a TypeId-keyed map populated once at boot:

let mut integrations = lona_rows::IntegrationDeps::new();
integrations.register::<dyn WeatherBackendDeps>(weather_deps);
integrations.register::<dyn StravaBackendDeps>(strava_deps);
// ... one register call per provider, no central deps.rs edit

The dispatcher resolves <R::Integration as Integration>::Deps to its registered &Arc<dyn ...> at runtime via IntegrationDeps::expect. Missing wirings panic at boot via IntegrationDeps::ensure_all_registered::<RowKeys>(), called once in service init — so a forgotten registration fails fast with the named trait, not silently in the background.

Adding a new provider therefore requires zero edits to lona-rows/src/deps.rs or any other central file in the host; only the provider's own crate plus one host-side register line is touched.