# How to Create and Configure a Pinia Store with Persistence in Celeris Web

> Learn how to create and configure a Pinia store with persistence in Celeris Web. Initialize Pinia, add a custom persistence plugin with encryption, and define stores with the persist option.

- Repository: [Kirk Lin/celeris-web](https://github.com/kirklin/celeris-web)
- Tags: how-to-guide
- Published: 2026-03-05

---

**To create and configure a Pinia store with persistence in Celeris Web, initialize a Pinia instance in [`apps/admin/src/store/index.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/index.ts), register the custom persistence plugin from [`store/plugin/persist.ts`](https://github.com/kirklin/celeris-web/blob/main/store/plugin/persist.ts) with optional AES encryption and key prefixing, then define individual stores using `defineStore` with a `persist` option that specifies which fields to save to `localStorage` or `sessionStorage`.**

Celeris Web uses **Pinia** as its state management library combined with **pinia-plugin-persistedstate** for automatic state persistence. This setup allows you to save specific store properties to browser storage with optional encryption, ensuring sensitive data remains secure across page reloads. The following guide walks through the exact implementation found in the Celeris Web repository, covering initialization, plugin configuration, and store definition patterns.

## Create the Pinia Instance

In [`apps/admin/src/store/index.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/index.ts), Celeris Web initializes a single Pinia instance that serves as the root state container for the entire Vue application. The file exports a `setupStore` function that installs Pinia on the Vue app instance and wires up the persistence plugin before the app mounts.

```typescript
import type { App } from "vue";
import { createPinia } from "pinia";
import { registerPiniaPersistPlugin } from "~/store/plugin/persist";

const store = createPinia();                     // 📦 Create Pinia
registerPiniaPersistPlugin(store);              // 🔌 Attach persistence plugin

export function setupStore(app: App<Element>) {
  app.use(store);                               // 🚀 Install on Vue app
}

export { store };

```

Key implementation details:

- `createPinia()` is called once to instantiate the root store.
- `registerPiniaPersistPlugin(store)` injects the persisted-state plugin with custom serialization logic.
- `setupStore` is invoked in the main application entry point to attach the store to the Vue app.

## Register the Persistence Plugin with Encryption

The persistence logic lives in [`apps/admin/src/store/plugin/persist.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/plugin/persist.ts). This file configures **pinia-plugin-persistedstate** with a custom serializer that optionally encrypts data using AES-256-CBC when `SHOULD_ENABLE_STORAGE_ENCRYPTION` is enabled. It also applies a global key prefix via `PERSIST_KEY_PREFIX` to avoid collisions with other applications sharing the same origin.

```typescript
import type { Pinia } from "pinia";
import type { Serializer } from "pinia-plugin-persistedstate";
import { createPersistedState } from "pinia-plugin-persistedstate";
import destr from "destr";
import { createStorageName, EncryptionFactory } from "@celeris/utils";
import type { GlobEnvConfig } from "@celeris/types";
import type { Encryption } from "@celeris/utils";

import {
  SHOULD_ENABLE_STORAGE_ENCRYPTION,
  STORAGE_CIPHER_IV,
  STORAGE_CIPHER_KEY,
} from "~/setting/encryptionSetting";

const persistEncryption: Encryption = EncryptionFactory.createAesEncryption({
  key: STORAGE_CIPHER_KEY,
  iv: STORAGE_CIPHER_IV,
});

export const PERSIST_KEY_PREFIX = createStorageName(
  <GlobEnvConfig>import.meta.env,
);

/* Custom serializer – encrypts when enabled */
function customSerializer(shouldEnableEncryption: boolean): Serializer {
  if (shouldEnableEncryption) {
    return {
      deserialize: (value) => {
        const decrypted = persistEncryption.decrypt(value);
        return destr(decrypted);
      },
      serialize: (value) => {
        const serialized = JSON.stringify(value);
        return persistEncryption.encrypt(serialized);
      },
    };
  }
  return {
    deserialize: (value) => destr(value),
    serialize: (value) => JSON.stringify(value),
  };
}

/* Register plugin */
export function registerPiniaPersistPlugin(pinia: Pinia) {
  pinia.use(
    createPersistedState(
      createPersistedStateOptions(PERSIST_KEY_PREFIX),
    ),
  );
}

/* Global options used by the plugin */
export function createPersistedStateOptions(keyPrefix: string) {
  return {
    storage: localStorage,
    key: (id) => `${keyPrefix}__${id}`,
    serializer: customSerializer(SHOULD_ENABLE_STORAGE_ENCRYPTION),
  };
}

```

Critical configuration points:

- **Key prefixing**: All storage keys are prefixed with `${PERSIST_KEY_PREFIX}__` to namespace Celeris Web data.
- **Conditional encryption**: The `customSerializer` function returns an encrypted serializer when `SHOULD_ENABLE_STORAGE_ENCRYPTION` is true, otherwise uses plain JSON.
- **Global defaults**: The plugin attaches to every store unless overridden by local `persist` options.

## Define a Store with Persistence Options

Individual stores declare their persistence behavior using the `persist` property inside `defineStore`. In [`apps/admin/src/store/modules/user.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/modules/user.ts), the user store specifies exactly which state fields to survive page reloads using the `pick` array and dynamically selects between `localStorage` and `sessionStorage` based on `DEFAULT_PROJECT_SETTING`.

```typescript
import { defineStore } from "pinia";
import { DEFAULT_PROJECT_SETTING } from "~/setting/projectSetting";
import { PermissionCacheTypeConstants } from "@celeris/constants";
import { APP_USER_STORE_ID } from "../constants";

export const useUserStore = defineStore(APP_USER_STORE_ID, {
  // 👇 Persistence config specific to this store
  persist: {
    // Only keep these fields in storage
    pick: ["userInfo", "token", "refreshToken", "roleList", "updatedAt"],
    // Choose storage based on a global project setting
    storage:
      DEFAULT_PROJECT_SETTING.permissionCacheType ===
      PermissionCacheTypeConstants.LOCAL_STORAGE
        ? localStorage
        : sessionStorage,
  },

  state: () => ({
    // ...state fields (shouldLoggedIn, userInfo, token, etc.)
  }),

  getters: { /* … */ },

  actions: { /* … */ },
});

```

Store-level persistence strategies:

- **Selective persistence**: The `pick` property limits what is saved, reducing storage size and preventing transient or sensitive data from leaking to disk.
- **Storage selection**: The store can switch between `localStorage` (persistent across sessions) and `sessionStorage` (cleared when the tab closes) based on project-wide security settings.

## Consume the Store in Components

Once configured, consuming the store follows standard Pinia patterns. Import the store composable and interact with state, getters, and actions. The persistence layer automatically handles serialization, encryption, and storage updates when state changes.

```typescript
<script setup lang="ts">
import { useUserStore } from "~/store/modules/user";

const userStore = useUserStore();

// Example: login and let persistence handle token storage
await userStore.login({ username, password, remember: true });
</script>

```

Because the store was defined with a `persist` block, after `login` the selected state (e.g., `token`) is automatically written to the configured storage and restored on page reload without manual intervention.

## Summary

- **Initialize once**: Create the Pinia instance in [`apps/admin/src/store/index.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/index.ts) and register the plugin before mounting the Vue app.
- **Secure by default**: Configure encryption and key prefixing in [`apps/admin/src/store/plugin/persist.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/plugin/persist.ts) to protect persisted data.
- **Selective persistence**: Use the `persist.pick` option in individual stores to control exactly which state fields survive reloads.
- **Flexible storage**: Toggle between `localStorage` and `sessionStorage` per store based on project settings or specific security requirements.

## Frequently Asked Questions

### How do I enable encryption for persisted Pinia stores in Celeris Web?

Set `SHOULD_ENABLE_STORAGE_ENCRYPTION` to `true` in your encryption settings file. The `customSerializer` function in [`apps/admin/src/store/plugin/persist.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/plugin/persist.ts) automatically detects this flag and wraps the storage methods with AES-256-CBC encryption using the configured `STORAGE_CIPHER_KEY` and `STORAGE_CIPHER_IV`.

### Can I choose between localStorage and sessionStorage for different stores?

Yes. In the `persist` configuration object of any store, set the `storage` property to either `localStorage` or `sessionStorage`. The user store in [`apps/admin/src/store/modules/user.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/modules/user.ts) demonstrates this by checking `DEFAULT_PROJECT_SETTING.permissionCacheType` to determine the appropriate storage mechanism at runtime.

### What is the purpose of the key prefix in Celeris Web's persistence setup?

The `PERSIST_KEY_PREFIX` generated in [`apps/admin/src/store/plugin/persist.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/plugin/persist.ts) namespaces all storage keys with a project-specific string derived from environment variables. This prevents key collisions when multiple applications or instances share the same domain and avoids accidental overwrites of unrelated data.

### How do I prevent specific state fields from being persisted?

Use the `pick` array in your store's `persist` option to explicitly whitelist only the fields you want saved. Any state property not listed in `pick` will remain in memory only and will not be written to `localStorage` or `sessionStorage` when the state updates. If you need to blacklist specific fields instead, use the `omit` property (provided by pinia-plugin-persistedstate) in your persist configuration.