Skip to main content

Sync preferences

When you sign in to PlanningSup, your settings and preferences automatically sync across all your devices. This is powered by a local-first sync system that uses last-write-wins conflict resolution.

What gets synced

The following preferences sync across devices when you’re signed in:
  • Your currently selected planning(s)
  • Custom planning groups (name + associated plannings)
  • Blocklist (hidden event titles)
  • Highlight teacher preference (dim events without teacher)
  • Event colors (lecture, lab, tutorial, other)
  • Show weekends in week view
  • Merge duplicate events
  • Theme (light / dark / auto)
  • Target timezone (if set)
View history (current date, selected view) is not synced — each device maintains its own navigation state.

How sync works

Local-first architecture

PlanningSup uses a local-first approach where:
  1. Local changes are immediate: When you update a setting, it’s saved to localStorage instantly
  2. Background sync to server: The change is debounced and pushed to the server in the background
  3. Conflict resolution on login: When you sign in on a new device, PlanningSup compares local and server timestamps using last-write-wins
// apps/web/src/composables/useUserPrefsSync.ts
const onSessionReady = async () => {
  const localMeta = getLocalMeta()
  const serverMeta = getServerMeta()
  const localTs = localMeta[key] ?? 0
  const serverTs = serverMeta[key] ?? 0

  // Strategy 1: Server is newer. Adopt server value.
  if (serverTs > localTs && serverRaw !== undefined) {
    setLocalValue(fromServerToLocal(serverRaw))
    setLocalMeta({ ...localMeta, [key]: serverTs })
  } else {
    // Strategy 2: Local is newer or tied. Push local value.
    await pushToServer(localVal)
  }
}
See apps/web/src/composables/useUserPrefsSync.ts:181-224 for the full implementation.

Timestamp-based conflict resolution

Each preference has a local timestamp (stored in localStorage) and a server timestamp (stored in the database). When conflicts occur:
  • Newer timestamp wins: The most recently modified value is kept
  • Local changes while offline: If you modify settings while offline, they’re pushed to the server when you reconnect
  • Server changes while offline: If another device updates the same setting, your device adopts the server value when you come online
// apps/web/src/composables/useUserPrefsSync.ts
watch(source, (newValue) => {
  // Always keep a local "last modified" timestamp
  if (!applyingServerValue) {
    const localMeta = getLocalMeta()
    setLocalMeta({ ...localMeta, [key]: Date.now() })
  }
  // Push to server after debounce
  void pushToServer(newValue as T)
})
The server replaces __STAMP__ with an authoritative timestamp in prefsMeta:
{
  "theme": 1709654321000,
  "colors": 1709654400000,
  "blocklist": 1709654500000
}

Debounced sync

To avoid excessive API calls, PlanningSup debounces sync requests:
// apps/web/src/composables/useSettings.ts
syncPref('colors', colors, {
  toServer: v => encodeColorsToString(v),
  normalizeLocal: v => normalizeColors(v),
  normalizeServer: raw => parseAndNormalizeColors(raw),
  fromServerToLocal: (raw) => parseAndNormalizeColors(raw),
  setLocal: v => (colors.value = v),
  debounce: 400, // Wait 400ms after last change
})
Different preferences have different debounce times:
  • highlightTeacher: 10ms (instant)
  • showWeekends: 10ms (instant)
  • blocklist: 250ms (typing delay)
  • colors: 400ms (color picker delay)
  • customGroups: 50ms (fast updates)

Signing in for sync

To enable cross-device sync, you need to sign in with a supported provider:
1

Click 'Sign in with Discord'

In the user menu or welcome screen
2

Authorize PlanningSup

Grant access to your Discord profile (we only read your username and avatar)
3

Sync starts automatically

Your settings are pulled from the server and merged with local changes

Viewing synced data

You can inspect your synced preferences in the browser console:
// View local metadata (timestamps)
JSON.parse(localStorage.getItem('userPrefsMeta'))

// View a specific preference
localStorage.getItem('settings.colors')
localStorage.getItem('settings.blocklist')
On the server side, preferences are stored in the user table as JSON fields:
-- apps/api/src/db/schemas/auth.ts
user: pgTable('user', {
  theme: varchar('theme', { length: 10 }),
  colors: text('colors'),
  blocklist: text('blocklist'),
  plannings: text('plannings'),
  customGroups: text('custom_groups'),
  prefsMeta: text('prefs_meta'), -- JSON: { theme: 1234567890, colors: 1234567890, ... }
})

Disabling sync

If you want to use PlanningSup without syncing:
1

Don't sign in

Use PlanningSup as a guest. All settings remain local.
2

Sign out to stop syncing

Click your avatar → “Déconnexion” (Sign out)
Local changes made while signed out are not lost. When you sign in again, they’re synced to the server if they’re newer than the server values.

Multi-device workflow example

Scenario: Laptop and phone

  1. On your laptop:
    • Select your planning and customize colors
    • Sign in with Discord
    • Settings sync to the server
  2. On your phone:
    • Install the PWA
    • Sign in with Discord
    • Your planning and colors appear automatically
  3. Make a change on your phone:
    • Add an event to the blocklist
    • The change syncs to the server
  4. Back on your laptop:
    • Refresh the page or wait for the next sync check
    • The blocklist updates automatically

Sync edge cases

Both devices offline

If you make changes on Device A while offline, then make different changes on Device B while offline, the last device to come online wins.
This can result in data loss if both devices modify the same preference while offline. Consider this a limitation of last-write-wins conflict resolution.

Race conditions

If you rapidly switch between devices and make changes, the sync system may receive requests out of order. PlanningSup uses optimistic locking to avoid redundant requests:
// apps/web/src/composables/useUserPrefsSync.ts
const pushToServer = async (value: T) => {
  const payloadValue = toServer(value)
  // Avoid redundant API calls if value hasn't changed
  if (jsonEqual(payloadValue, lastSynced.value)) return
  lastSynced.value = payloadValue
  await authClient.updateUser(payload)
}

Clearing server data

To reset your synced preferences:
1

Sign out

Click your avatar → “Déconnexion”
2

Clear local data

Open browser settings → Clear site data for planningsup.app
3

Sign in again

Your preferences reset to defaults (no server data exists)
There is no “Delete account” feature yet. Signing out and clearing local data effectively resets your account to a blank state.

Authentication system

PlanningSup uses Better Auth for authentication, with the following features:
  • OAuth 2.0: Discord and GitHub providers
  • Passkeys (WebAuthn): For passwordless login on supported browsers
  • Session cookies: Secure, HTTP-only cookies with automatic refresh
  • Deep linking: For Tauri and browser extension OAuth flows
See apps/web/src/composables/useAuth.ts and apps/api/src/utils/auth.ts for the implementation.

Next steps

Web app

Learn about the web version features

Desktop & mobile

Install the Tauri-based native apps

Build docs developers (and LLMs) love