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

- Date: 2026-06-15
- Author: Jeppe Spanggaard
- Description: Learn how to store Outlook add-in user preferences in the OneDrive App Folder via the Graph approot endpoint, so settings roam across every device.
- URL: https://jeppe-spanggaard.dk/blogs/outlook-addin-onedrive-approot-preferences/
- Tags: Microsoft Graph, Outlook Add-ins

## TL;DR

Microsoft Graph gives every app its own private folder in the user's OneDrive: the **App Folder**, reachable at `/me/drive/special/approot`. Drop a `user-settings.json` in there. Preferences now follow the user to desktop, web, and mobile, and with the `Files.ReadWrite.AppFolder` permission your app can't touch anything else in their drive.


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:

```typescript
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:

```typescript
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.

