Kontakt os

Your Add-in Gets a Free Folder in Everyone's OneDrive (Use It)

My Outlook add-in has a handful of user preferences: which tab to open by default, language, a “don’t show this tip again” flag. Small stuff. A single JSON blob.

So where do you put it? localStorage is per-device, per-browser, and mobile WebViews evict it whenever they feel like it. A backend with a database is a lot of infrastructure for one JSON file per user. And then it hit me: every single one of my users already has cloud storage my add-in can reach. Their OneDrive.

What the App Folder Actually Is

First time your app touches approot, OneDrive creates a folder under Apps/<your app's name> (the name comes from your Entra app registration). It’s the user’s storage, visible to them in OneDrive, but sandboxed for you: request Files.ReadWrite.AppFolder and that folder is all your app can see. No scary “this app can read all your files” consent screen.

One endpoint to remember:

/me/drive/special/approot

Saving Settings

I use PnPjs (@pnp/graph), so a save is three lines:

import { SpecialFolder } from "@pnp/graph/files";

async function saveUserSettings(settings: UserDefinedSettings): Promise<void> {
  const appRoot = graphFI.me.drive.special(SpecialFolder.AppRoot);
  await appRoot.upload({
    content: JSON.stringify(settings),
    filePathName: "user-settings.json",
    contentType: "application/json",
  });
}

No PnPjs? The raw Graph equivalent is a single PUT: PUT /me/drive/special/approot:/user-settings.json:/content.

Reading Settings (and Surviving the First Run)

Reading has one twist: the very first time a user opens your add-in, the file doesn’t exist yet. That’s not an error, that’s a new user. Plan for it:

const defaultSettings: UserDefinedSettings = {
  openPreviewInNewTab: false,
  returnToHomeAfterArchive: false,
  hasSeenPinTip: false,
};

async function getUserSettings(): Promise<UserDefinedSettings> {
  try {
    const appRoot = graphFI.me.drive.special(SpecialFolder.AppRoot);
    const children = await appRoot.children();
    const fileItem = children.find((item) => item.name === "user-settings.json");

    if (!fileItem) {
      return defaultSettings; // first run - no file yet
    }

    const blob = await graphFI.me.drive.getItemById(fileItem.id!).getContent();
    const json = JSON.parse(await blob.text());

    return {
      openPreviewInNewTab: json.openPreviewInNewTab ?? defaultSettings.openPreviewInNewTab,
      returnToHomeAfterArchive: json.returnToHomeAfterArchive ?? defaultSettings.returnToHomeAfterArchive,
      hasSeenPinTip: json.hasSeenPinTip ?? defaultSettings.hasSeenPinTip,
    };
  } catch (error) {
    console.error("Error getting user settings:", error);
    return defaultSettings; // any failure - the add-in still works
  }
}

What’s happening here?

  1. Missing file → defaults, not an exception. New users get a working add-in, not an error toast.
  2. Every field is parsed with a ?? fallback. When I ship a new setting next month, users with an old user-settings.json don’t break, the new field just gets its default.
  3. The whole thing is wrapped in try/catch that returns defaults. Settings are a nice-to-have; they should never take the add-in down with them.

Two practical habits: load the settings once at boot and cache them (don’t re-read OneDrive on every render), and save optimistically - update your local state immediately, fire the upload without awaiting it. And the best part comes for free: change a setting on desktop, open the phone, it’s there. 🎁

Not Just Outlook Add-ins: SPFx Too

This pattern isn’t tied to Outlook at all, it just needs a user context and a Graph token. That makes it a great fit for SPFx web parts as well.

Web part properties in SPFx are per-instance and per-page, and usually something an editor configures, not the end user. If you want user-level preferences - a collapsed/expanded state, a preferred view, a dismissed banner - that follow the user across every page and site where your web part lives, the App Folder solves it with zero extra infrastructure. Grab MSGraphClientV3 from the SPFx context and hit the same /me/drive/special/approot:/user-settings.json:/content endpoint. Same folder, same JSON file, same permission model.

Gotchas

  • Ask for Files.ReadWrite.AppFolder, not Files.ReadWrite. If the app folder is all you need, the scoped permission gets you a much friendlier consent prompt and a much smaller blast radius.
  • The folder is named after your app registration’s display name. Rename the registration and users get a new empty folder, your settings file stays behind in the old one.
  • Users can see (and delete) the folder. It’s their OneDrive. Treat a missing file as a normal state, always, not just on first run.

Wrapping Up

User preferences don’t need a database, and they deserve better than localStorage. The OneDrive App Folder is the middle ground that’s easy to miss: zero infrastructure on your side, real cloud persistence on theirs, and a permission model that only exposes what your app actually needs. Outlook add-in, SPFx web part, anything with a Graph token - same trick everywhere.

Pinksky bruger cookies for at forbedre funktionaliteten på denne hjemmeside. Du bestemmer selv hvad vi må bruge. Læs mere i vores cookiepolitik.