State Persistence

Two slices are persisted to localStorage through a shared meta-reducer factory: auth and marketplaces. Everything else is re-fetched on load.

The meta-reducer factory

src/app/core/reducers/persistent-store.meta-reducer-factory.ts

export const storageMetaReducer = <S, A extends Action = Action>(
    saveKeys: string[],
    localStorageKey: string,
    storageService: PersistentStateManager,
) => {
    let onInit = true;
    return (reducer: ActionReducer<S, A>) => (state: S, action: A): S => {
        const nextState = reducer(state, action);

        if (onInit) {                                              (1)
            onInit = false;
            const savedState = storageService.getSavedState(localStorageKey);
            return merge(nextState, savedState);                   (2)
        }

        const stateToSave = pick(nextState, saveKeys);             (3)
        storageService.setSavedState(stateToSave, localStorageKey);
        return nextState;
    };
};
1 The very first action through the reducer is treated as hydration.
2 lodash.merge deep-merges the saved snapshot over the freshly computed state, so persisted values win.
3 On every subsequent action, only the listed keys are written back.

PersistentStateManager

src/app/core/pyb/services/persistent-state-manager.service.ts

@Injectable({ providedIn: 'root' })
export class PersistentStateManager {
    private storageKeys = {};

    setSavedState(state: any, localStorageKey: string): void {
        this.storageKeys[localStorageKey] = true;
        localStorage.setItem(localStorageKey, JSON.stringify(state));
    }

    getSavedState(localStorageKey: string): any {
        return JSON.parse(localStorage.getItem(localStorageKey));
    }

    clear() {
        for (const key in this.storageKeys) {
            if (typeof key === 'string') { localStorage.removeItem(key); }
        }
        this.storageKeys = {};
    }
}

clear() only removes keys this instance has written. It is called from AuthEffects.logout$, which is followed by an explicit localStorage.removeItem(authTokenKey) and a full page reload — belt and braces.

Persisted slices

auth

Configured in AuthModule:

{ provide: AUTH_LOCAL_STORAGE_KEY, useValue: '__auth_storage__' },
{ provide: AUTH_STORAGE_KEYS,      useValue: ['auth'] },
{
    provide: AUTH_CONFIG_TOKEN,
    deps: [AUTH_STORAGE_KEYS, AUTH_LOCAL_STORAGE_KEY, PersistentStateManager],
    useFactory: getAuthPersistentStateFactory,
},

Storage key

auth_storage

Saved keys

['auth'] — the LoginState sub-slice only

Not saved

recoverPasswordRequest, newPasswordRequest (transient form state)

This is what makes AuthGuard able to read isLoggedIn from the store on a cold load: the slice is rehydrated before the router evaluates guards.

marketplaces

Configured in MarketplacesFeatureModule:

Storage key

marketplaces_storage

Saved keys

['ids', 'entities'] — the @ngrx/entity collection itself

Not saved

Loading and error flags

The marketplace catalog is large, effectively static reference data, and needed early to render the marketplace pages — caching it removes a blocking request from every page load.

The session token

The token is not part of the persisted slice. It is written directly by AuthEffects.login$ and register$ to localStorage[environment.authTokenKey], and read by AuthService.getAccessToken() and AuthEffects.init$.

Other localStorage keys

Key Owner / purpose

environment.authTokenKey

The session token.

auth_storage

Persisted auth slice.

marketplaces_storage

Persisted marketplaces entity collection.

docusignFlowInitialized

Replay guard for DocuSign returns. See DocuSign Contract Flows.

link_token

Plaid Link token, read and removed by AppModule.handlePlaid().

Caveats

Hydration merges, it does not replace

merge(nextState, savedState) means a stale persisted value overrides a fresh one on the first action. Removing a field from a state interface does not remove it from previously persisted snapshots, and a renamed field leaves the old key merged in. When changing the shape of a persisted slice, bump the storage key (for example auth_storage_v2) so old snapshots are ignored.

The onInit flag is per-factory-call

let onInit = true is captured in the closure created by storageMetaReducer(…). Each slice gets its own flag because each calls the factory separately. Reusing one returned meta-reducer across two slices would hydrate only the first.

Every action writes to localStorage

The save branch runs on every dispatched action, serializing the picked keys each time. It is acceptable at the current slice sizes, but persisting a large or rapidly changing slice would put JSON serialization on the hot path.

Persisted state is never trusted for authorization

On boot, AuthEffects.init$ re-enters the full login pipeline: the user is re-fetched from the API and permissions are recalculated by PermissionsService. Tampering with auth_storage changes what renders for a fraction of a second, not what the API will authorize.