Task 7.3: applies the MODIFIED + ADDED requirements from
changes/deepen-connected-services/specs/ into openspec/specs/:
- connected-services/spec.md:
- MODIFIED: OAuth token storage (renamed sync_connections → connected_services
with credential_kind discriminator + JSONB credentials shape).
- ADDED: Capability seams for providers (Importer / RoutePusher /
WebhookReceiver per ADR-0002, no unified SyncProvider).
- ADDED: Centralized credential lifecycle via ConnectedServiceManager.
- wahoo-import/spec.md: Provider-agnostic framework rewritten to reference
capability seams + per-provider manifest. Token refresh now goes through
withFreshCredentials. Renames sync_connections → connected_services
throughout.
- wahoo-route-push/spec.md: Renames sync_connections → connected_services.
ADDED: RoutePusher capability seam — shape (service, route) →
{remoteId, version} per ADR-0003; Wahoo workarounds stay inside the
adapter.
Change directory moved to openspec/changes/archive/2026-05-08-deepen-connected-services/.
29/29 tasks complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.6 KiB
Why
The current SyncProvider interface (apps/journal/app/lib/sync/types.ts:88-118) is OAuth-shaped: it bakes getAuthUrl, exchangeCode, refreshToken, parseWebhook, and pushRoute? into one fat interface. Wahoo is the only adapter today, and the abstraction is a hypothetical seam. With Komoot (web-login, no official API) and Apple Health (mobile-pushed, no remote credential) both in flight, that seam is about to break: a unified interface fills with NOPs and forces every future provider to inherit OAuth assumptions.
This change reshapes the seam before the second and third adapters land — so Komoot and Apple Health fit cleanly instead of distorting the architecture. Decisions are recorded in docs/adr/0001-0003; vocabulary in CONTEXT.md.
What Changes
- BREAKING (DB): rename
journal.sync_connections→journal.connected_services. Addcredential_kindtext column (discriminator:oauth | web-login | device) andcredentialsJSONB column. Backfill existing Wahoo rows:credential_kind='oauth', moveaccess_token / refresh_token / expires_atinto the JSONB blob. Drop the old token columns. - BREAKING (code): replace
SyncProviderinterface with three capability seams:Importer,RoutePusher,WebhookReceiver. Each provider declares which capabilities it implements via a co-located manifest (providers/<name>/manifest.ts); a smallproviders/registry.tsimports each. - New module
ConnectedServiceManageratapps/journal/app/lib/connected-services/manager.tsowning credential lifecycle:link / unlink / withFreshCredentials / markNeedsRelink. Centralizes token-refresh logic currently inlined inpushes.server.ts(~lines 145-170). - New per-kind module
CredentialAdapter.oauthadapter ships with this change (refresh / revoke).web-loginadapter lands withkomoot-import;deviceadapter lands withmobile-app. Thecredential_kindenum carries all three from day one to avoid a follow-up migration. RoutePusherseam shape:pushRoute(service, route) → {remoteId, version}. Wahoo's FIT-Course conversion, theroute:<id>external_idconvention, and the PUT→POST-on-404 fallback stay inside the Wahoo adapter — they are not part of the seam (per ADR-0003).- Wahoo's existing code splits into
providers/wahoo/{importer.ts, pusher.ts, webhook.ts, manifest.ts}; existing tests reorganize accordingly. - Spec deltas: rename
sync_connections→connected_servicesinconnected-services/spec.md,wahoo-import/spec.md,wahoo-route-push/spec.md. Add capability-seam terminology toconnected-services/spec.md.
Capabilities
New Capabilities
(none — this change reshapes existing capabilities, it does not introduce new user-visible behaviour.)
Modified Capabilities
connected-services: requirement-level changes — credential storage shape (credential_kinddiscriminator + JSONB), provider model (capability seams instead of unifiedSyncProvider), token-refresh policy (centralized viaConnectedServiceManager.withFreshCredentials).wahoo-import: rename of underlying table and refactor of how the importer obtains credentials (via manager, not via direct table read). User-visible behaviour unchanged.wahoo-route-push: rename of underlying table;RoutePusherseam shape codified. User-visible behaviour unchanged.
Impact
- Code:
apps/journal/app/lib/sync/is replaced byapps/journal/app/lib/connected-services/(manager + credential adapters + registry) andapps/journal/app/lib/connected-services/providers/wahoo/(split capability modules). Routes and jobs that read sync data update their imports. - DB: one migration — rename + add columns + backfill + drop legacy columns. Production
connected_servicesrow count is small (sandbox era); backfill is online-safe. - Tests:
wahoo.test.ts(285 lines) andpushes.server.test.ts(309 lines) reorganize to test capabilities (importer.test.ts,pusher.test.ts,webhook.test.ts) against a fakeConnectedServiceManageryielding stub credentials. Newmanager.test.tscovers refresh / needs_relink transitions. - Specs: 3 spec files updated (table rename + terminology). No removals.
- Out of scope (separate follow-on changes):
komoot-import(will be amended to use this architecture instead of its draftedjournal.integrationstable);mobile-app(Apple Health provider lands there). Neither is touched by this change beyond a reference. - Decision references:
docs/adr/0001-connected-services-credential-discriminator.md,docs/adr/0002-no-unified-syncprovider-interface.md,docs/adr/0003-routepusher-seam-shape.md,CONTEXT.md(Connected Services section).