Isolated Storage – BC’s keeper of secrets

In this video, I show how to use the Isolated Storage feature in Microsoft Dynamics 365 Business Central to store a password:

https://youtu.be/KEBRbTbTCyI

In this video, Erik walks through Isolated Storage in Business Central — a built-in mechanism for storing secrets like passwords, access tokens, and credentials so they’re hidden from other extensions, OData integrations, and prying eyes. He builds a complete example from scratch: a setup table, a setup page, and demonstrates how to use the IsolatedStorage.Set and IsolatedStorage.Get methods with proper data scoping.

What Is Isolated Storage?

Isolated Storage is a key-value data store available in Business Central that allows an extension to store sensitive information outside of normal database tables. The concept isn’t new — it has existed in .NET for a long time as a way to store data specific to a single application. In Business Central, it works the same way but is scoped to a single extension.

Normally, when you store information in a table, it’s accessible to other extensions, customization code, and data integration tools like OData. Isolated Storage is different. While the data still lives in the database technically, it’s stored on the side and is not accessible through other means. Only the extension that wrote the data can read it back.

Erik and his team use Isolated Storage extensively in their apps to store credentials, access tokens, and other sensitive information that should not be visible to anyone else.

Building the Example: A Setup Table

To demonstrate, Erik creates a simple setup table with a primary key and a username field. The important design decision here is that the password field is intentionally omitted from the table. Instead of storing the password in a regular table field, it will be stored using Isolated Storage.

table 59300 "ISO Setup"
{
    Caption = 'ISO Setup';
    DataClassification = ToBeClassified;

    fields
    {
        field(1; PKey; Code[10])
        {
            Caption = 'PKey';
            DataClassification = SystemMetadata;
        }
        field(10; "User Name"; Text[30])
        {
            Caption = 'User Name';
            DataClassification = SystemMetadata;
        }
        // field(11; Password; Text[30])
        // {
        //     Caption = 'Password';
        //     DataClassification = SystemMetadata;
        // }
    }
    keys
    {
        key(PK; PKey)
        {
            Clustered = true;
        }
    }
}

Notice the commented-out password field — this is what we’re replacing with Isolated Storage.

The Isolated Storage API: Set and Get

A Common Pitfall: Don’t Use the Codeunit

One thing Erik calls out is a confusing aspect of the API. You might be tempted to use the Isolated Storage Management codeunit that exists in the system. Don’t. If you try to use it, you’ll get an error saying “the type of method that cannot be used for extension development.” Instead, use the built-in IsolatedStorage type directly — it’s a magic type available in AL without any codeunit reference.

The Setter (Storing a Password)

The Set function takes three parameters: a key (string), a value, and a data scope. Erik prefers clear-text keys so the code is readable:

procedure Password(pw: Text): Text
begin
    IsolatedStorage.Set('password', pw, DataScope::Company);
    exit(pw);
end;

The Getter (Retrieving a Password)

The Get function also takes a key, a data scope, and an output variable. Importantly, Get returns a boolean — if you don’t check it, you’ll get a runtime error. Also, the data scope used in Get must match the one used in Set, or the retrieval will fail:

procedure Password(): Text
var
    pw: Text;
begin
    if IsolatedStorage.Get('password', DataScope::Company, pw) then
        exit(pw);
end;

Thanks to AL’s support for overloads, both procedures can share the name Password — one with a parameter (the setter) and one without (the getter).

Understanding Data Scope

The DataScope parameter is critical. It determines who can see the stored secret and at what level it’s shared. There are four options:

  • Module — The secret is available to the extension across all companies and all users. Despite the confusing name, “module” simply means “app” or “extension.”
  • User — The secret is specific to the current user, but still across all companies. Different users can store different values under the same key.
  • Company — The secret is specific to the current company, but shared across all users within that company.
  • CompanyAndUser — The most restrictive scope. The secret is specific to both the current company and the current user.

In this example, Erik uses DataScope::Company because he’s replacing a password field in a setup table — something that’s typically per-company but shared among users of that company.

Building the Setup Page

The page is where most of the complexity lives. Since the password isn’t a real field on the table, there are several UI challenges to solve:

Ensuring a Record Exists

A behavioral difference between NAV and Business Central is that single-record card pages can appear editable even when no record exists, leading to data that looks saved but isn’t. The fix is simple — auto-insert a blank record on page open:

trigger OnOpenPage()
begin
    PageEdit := CurrPage.Editable;
    if IsEmpty() then
        Insert();
end;

Making the Virtual Field Editable

Because the password is backed by a variable rather than a table field, the page won’t make it editable by default. Erik works around this by tracking the page’s editable state in a boolean variable and binding it to the field’s Editable property. He notes that ideally you’d call CurrPage.Editable directly in the property, but AL doesn’t allow procedure calls there:

field(Password; pw)
{
    ApplicationArea = All;
    Editable = PageEdit;
    ExtendedDatatype = Masked;
    trigger OnValidate()
    begin
        Password(pw);
    end;
}

Loading and Saving the Value

On OnAfterGetRecord, the password variable is populated from Isolated Storage. On validate, it’s written back:

trigger OnAfterGetRecord()
begin
    PageEdit := CurrPage.Editable;
    pw := Password();
end;

The Complete Page

Here’s the full page code, including a test action that displays the stored password to verify everything works:

page 59300 "ISO Setup"
{
    Caption = 'ISO Setup';
    PageType = Card;
    SourceTable = "ISO Setup";

    layout
    {
        area(content)
        {
            group("Login Information")
            {
                field("User Name"; "User Name")
                {
                    ApplicationArea = All;
                }
                field(Password; pw)
                {
                    ApplicationArea = All;
                    Editable = PageEdit;
                    ExtendedDatatype = Masked;
                    trigger OnValidate()
                    begin
                        Password(pw);
                    end;
                }
            }
        }
    }
    actions
    {
        area(Processing)
        {
            action(WhatPassword)
            {
                Caption = 'What is my password';
                ApplicationArea = All;
                trigger OnAction()
                begin
                    Message(Password());
                end;
            }
        }
    }
    trigger OnAfterGetRecord()
    begin
        PageEdit := CurrPage.Editable;
        pw := Password();
    end;

    trigger OnOpenPage()
    begin
        PageEdit := CurrPage.Editable;
        if IsEmpty() then
            Insert();
    end;

    var
        [InDataSet]
        pw: Text;
        PageEdit: Boolean;
}

Key Takeaways

  • Isolated Storage is extension-scoped. No other app can read your stored values — they’re completely invisible to other extensions, OData, and data integration tools.
  • It’s a key-value store. You set and get values by string keys. Erik recommends using clear, readable key names.
  • Use IsolatedStorage directly, not the Isolated Storage Management codeunit — the codeunit will throw errors in extension development.
  • Data scope matters. Choose between Module, User, Company, or CompanyAndUser depending on how broadly the secret should be shared. Make sure your Get and Set calls use the same scope.
  • The storage behind the scenes uses a blob field. It’s not designed for thousands of records, but it’s very reliable and easy to work with for credentials and tokens.
  • The core implementation is just two lines of code — one IsolatedStorage.Set and one IsolatedStorage.Get. Most of the effort goes into making the page UI behave correctly with virtual fields.

Isolated Storage is a straightforward yet powerful feature for keeping sensitive data safe in Business Central. Whether you’re storing passwords, API keys, access tokens, or other secrets, it provides a clean, secure mechanism that’s purpose-built for extension development.