Synchronization Model¶
Mapping¶
Every synchronized entry is tracked as a WorklogMapping:
interface WorklogMapping {
jiraIssueId: string;
jiraIssueKey: string;
jiraWorklogId: string;
kimaiTimesheetId: number;
origin: 'jira' | 'kimai';
lastSyncedAt: string;
lastHash?: string;
}
It is stored twice, indexed from both directions (storage/mappings.ts), so either system can
look up its counterpart in O(1).
Jira → Kimai¶
- Jira emits
avi:jira:created:worklog/avi:jira:updated:worklog. src/jira/worklog-events.tsresolves the Kimai user/project/activity for the worklog author.src/sync/jira-to-kimai.tscreates or updates the mapped Kimai timesheet.- The mapping is persisted with a content hash of the synced fields.
Kimai delete events are deferred until a reliable webhook event is available upstream; the
handler layer (src/webhooks/handlers/) isolates this so delete support can be added without
changing the synchronization architecture (see also the note under "Kimai to Jira" below).
Kimai to Jira¶
- Kimai calls the Forge web trigger with a signed
timesheet.created/timesheet.updatedpayload. src/webhooks/kimai-webhook.tsverifies the HMAC signature before processing anything.src/sync/kimai-to-jira.tscreates or updates the mapped Jira worklog.
timesheet.deleted is not yet propagated from Kimai to Jira; support will be added once the
corresponding Kimai webhook event is available/reliable.
Failure semantics¶
Synchronization is not atomic across Jira and Kimai. The source record has already been saved before its event reaches Forge, so a failed target write leaves that source record unchanged. A mapping is persisted only after the target write succeeds. For a mapping-storage failure after a new target record is created, a pending-create record lets the next replay restore the mapping; the integration does not delete either record as compensation. See the detailed success and failure paths in Architecture.
Idempotency and loop prevention¶
- Idempotency:
src/sync/idempotency.tscomputes a stable content hash for each change. Replayed events with an unchanged hash are skipped (shouldSkipSyncEvent). - Loop prevention: Jira worklog events include a
selfGeneratedflag; when set, the event is ignored because it was caused by our own Kimai → Jira write. Combined with the persisted mapping, this prevents infinite create/update loops between the two systems.
Conflict resolution¶
The MVP policy is "last accepted update wins" (src/sync/conflict-resolution.ts). Every applied
change is logged with its source, timestamp, previous hash and new hash so a future version can
build a proper conflict-resolution UI without changing the underlying data model.