Microsoft / Outlook
Outlook sync is a two-way adapter in
apps/api/src/sync/adapters/microsoft.ts, built on Microsoft Graph v1.0. It
supports multiple Microsoft accounts per Musubi user, but Graph’s event delta
semantics create important differences from Google.
Configure Entra ID
Section titled “Configure Entra ID”-
Create an app registration.
Use a multitenant registration when both organizational and personal Microsoft accounts should work.
-
Add a Web redirect URI.
{BETTER_AUTH_URL}/api/auth/callback/microsoftThis must be a Web platform redirect, not SPA or Mobile. Musubi performs a server-side authorization-code exchange using the client secret.
-
Grant the required scopes.
openid User.Read Calendars.ReadWrite offline_access -
Configure and restart the API.
MICROSOFT_CLIENT_ID=...MICROSOFT_CLIENT_SECRET=...MICROSOFT_TENANT_ID=commoncommonaccepts organizational and personal accounts. Set one tenant ID only when the deployment should be organization-restricted. -
Verify discovery.
/api/v1/servershould listmicrosoftinsyncProviders.
Microsoft may rotate its refresh token. The shared OAuth helper persists the replacement token; an adapter change that discards it will eventually break the connection.
Windowed change tracking
Section titled “Windowed change tracking”Graph v1.0 exposes event delta through calendarView, whose date range is baked
into the delta token. Musubi uses:
- 180 days in the past;
- 730 days in the future; and
- a full-window renewal when the future edge is within 90 days.
An expired delta token (410) also triggers a reset pull. Events outside this
window are not mirrored. This is a provider constraint, not a client filter.
Requests prefer UTC timestamps and plain-text bodies:
Prefer: outlook.timezone="UTC", outlook.body-content-type="text"Recurring event model
Section titled “Recurring event model”calendarView returns expanded occurrences and exceptions rather than a
portable RRULE series. A recurring occurrence may be only a lightweight stub:
subject, all-day state, body, and other fields live on its series master.
The adapter therefore:
- groups occurrences by
seriesMasterId; - fetches each missing master once per sync run;
- fills absent fields from the master; and
- preserves occurrence-specific ID, start/end, and exception overrides.
Cost: one extra Graph request per unique series during a full sync.
Other mappings
Section titled “Other mappings”- Graph all-day end is exclusive; Musubi stores an inclusive end, so pull subtracts one day.
- Graph calendar colors are a preset enum. The adapter maps arbitrary Musubi hex values to the nearest supported preset.
- Calendar create, update, event write, and delete use the account connected to that calendar.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause and response |
|---|---|
AADSTS90023 |
Redirect URI missing, mismatched, or registered as SPA. Add the exact URI under Web. |
AADSTS7000215 |
Secret is wrong or expired. Copy the secret value, not its ID. |
OAuth page says invalid_code |
Inspect API logs for the underlying AADSTS… code and matching request ID. |
| Recurring items are untitled/timed | Master hydration failed or stub detection regressed. |
| Older/far-future item is absent | It may be outside the 180/730-day window. |
| Color differs slightly | Graph accepted the nearest provider preset. |
Portal changes can take a short time to propagate. Do not repeatedly rotate secrets until the exact redirect and platform type have been verified.
Contributor checklist
Section titled “Contributor checklist”- test initial, delta, renewed-window, and
410reset pulls; - retain rotated refresh tokens;
- cover lightweight occurrence, exception, all-day series, and missing master;
- keep the fixed-window limitation explicit in UI and docs;
- reject unsupported recurrence without partial writes; and
- run
pnpm check(including fake Graph pagination/410/master cache reset),pnpm test:provider-dbfor OAuth changes, plus a sandbox-account round trip before release.