Skip to content

Deep-Dive: Settings & AppConfig Data Flow Analysis

Date: 2026-02-25 Scope: Full tracing of how Setting and AppConfig data originates, flows through the backend API, reaches the frontend via API hooks, enters Redux, and is consumed by every component in the application.


TABLE OF CONTENTS

  1. Backend Entities & Storage
  2. Backend API Layer
  3. Backend Consumers of Settings
  4. Frontend API Hooks Layer
  5. Redux Store Layer
  6. SSR Settings Access (Server Components)
  7. CSR Settings Access (Client Components)
  8. Complete Consumer Component Registry
  9. Complete Setting Key Registry
  10. Settings Data Flow Map
  11. Caching Strategy Analysis
  12. Error States & Fallback Analysis
  13. Public vs Authenticated Access Patterns
  14. AppConfig: Separate System
  15. Issues Found

1. BACKEND ENTITIES & STORAGE

1.1 Setting Entity (saas-boilerplate/src/entity/Setting.ts)

@Entity("settings")
export class Setting {
  @PrimaryGeneratedColumn()           id: number;
  @Column({ varchar, 255 })           key: string;        // e.g., "site.footer_copyright"
  @Column({ varchar, 255, nullable }) display_name: string; // e.g., "Footer Copyright"
  @Column({ text, nullable })         value: string;       // The actual setting value
  @Column({ text, nullable })         details: string;     // JSON string for select_dropdown options
  @Column("simple-array", nullable)   backupCodes: string[]; // MISPLACED - belongs on UserSetting
  @Column({ varchar, 255, nullable }) type: string;        // "text"|"textarea"|"select_dropdown"|"image"|"checkbox"|"input"
  @Column({ int, nullable, default:1})order: number;       // Display order within group
  @Column({ varchar, 255, nullable }) group: string;       // Grouping label: "site"|"admin"|"auth"|"contact"
  @DeleteDateColumn({ nullable })     deletedAt?: Date;    // Soft delete
}

Key design: Settings use a dot-notation key (group.field_name) where the group prefix matches the group column. This is the foundation of the entire grouping/prefix system.

1.2 AppConfig Entity (saas-boilerplate/src/entity/AppConfig.ts)

@Entity("app_configs")
@Index(["key"], { unique: true })
export class AppConfig {
  @PrimaryGeneratedColumn()           id: number;
  @Column({ varchar, 150 })           key: string;        // e.g., "stripe_secret_key"
  @Column({ varchar, 255 })           displayName: string;
  @Column({ text })                   value: string;       // Potentially encrypted
  @Column({ default: true })          isEncrypted: boolean;
  @CreateDateColumn()                 createdAt: Date;
  @UpdateDateColumn()                 updatedAt: Date;
  @DeleteDateColumn({ nullable })     deletedAt?: Date;
}

Key difference: AppConfig is for server-side secrets (API keys, encrypted values). Setting is for UI-facing configuration. AppConfig is NEVER exposed to the frontend.

1.3 Seed Data (saas-boilerplate/src/seed/data/setting.ts)

17 settings are seeded across 4 groups:

Group Key Type Seeded Value
admin admin.site_name input "SASS Boilerplate"
admin admin.test_5 select_dropdown "option_2"
admin admin.favicon image Cloudinary URL
admin admin.meta_description textarea Long description text
admin admin.meta_image image Cloudinary URL
auth auth.login_page_type select_dropdown "1" (options: ½/3)
auth auth.register_page_type select_dropdown "2" (options: ½/3)
auth auth.login_image image Cloudinary URL
auth auth.register_image image Cloudinary URL
auth auth.website_type select_dropdown "2" (1=default, 2=invite only)
contact contact.contact_address input Florida HQ address
contact contact.page_desc textarea Contact page description
contact contact.contact_phone input "(800) 690-3072"
contact contact.contact_email input "sass@gmail.com"
contact contact.page_title input "We're here to help you"
site site.footer_copyright input "2025 SaaSify..."
site site.site_logo image Cloudinary URL
site site.site_logo_dark image Cloudinary URL
site site.footer_text textarea Footer description text

NOT seeded but consumed by frontend (see Issues): - admin.meta_title -- used by root layout generateMetadata() - admin.admin_site_title -- used by dashboard layout <title> tag - site.footer_site_link -- used by footer-credit.jsx - site.footer_site_name -- used by footer-credit.jsx - site.footer_heart_link -- used by footer-credit.jsx - site.site_name -- used by new sidebar fallback text - admin.image_size -- referenced in setting.service.ts getImageSizeByKey()


2. BACKEND API LAYER

2.1 Setting Routes (saas-boilerplate/src/app/modules/v1/Setting/setting.routes.ts)

Mounted at /api/v1/settings.

Method Path Auth Required Controller Method Description
GET / NO getAllSettings Returns ALL settings
GET /:prefix/prefix NO getSettingByPrefix Returns settings matching prefix via LIKE
GET /:id NO getSettingById Returns single setting by ID
POST / YES createSetting Creates new setting (multipart)
PATCH /bulk-update YES updateSettings Bulk update by key
PATCH /:id YES updateSettingById Update single setting (multipart)
DELETE /:id YES deleteSetting Soft delete setting

CRITICAL FINDING: The GET endpoints (/, /:prefix/prefix, /:id) have NO authentication. The auth() middleware is applied AFTER the GET routes via router.use(auth(), contextMiddleware). This means ALL settings (including any sensitive ones) are publicly readable.

2.2 Setting Service Key Methods

  • getAllSettings(): Returns all non-deleted settings. No pagination, no filtering. Used by the GET / endpoint.
  • getSettingsByPrefix(prefix): Uses LIKE '${prefix}%' query. Used by GET /:prefix/prefix. This is the primary method the frontend uses for grouped access.
  • getSettingByKey(key): Exact key lookup. Used internally by auth service and registration policy.
  • getImageSizeByKey(): Hardcoded to look up admin.image_size. Returns numeric value or default 5. Defined but NEVER called anywhere in the codebase.

2.3 AppConfig Routes (saas-boilerplate/src/app/modules/v1/AppConfig/app_config.routes.ts)

Mounted at /api/v1/app-configs.

Method Path Auth Required Permission Check
GET / NO (!) app_config.read in service
GET /:id NO (!) app_config.read in service
POST / YES create_app_config.create
PATCH /:id YES update_app_config.update
DELETE /:id YES app_config.delete

Note: GET routes lack route-level auth but have service-level permission checks via checkPermissionAndThrow. This means unauthenticated users will fail at the service layer, not the route layer.


3. BACKEND CONSUMERS OF SETTINGS

3.1 Auth Service (auth.service.ts)

Setting consumed: auth.website_type How: SettingService.getSettingByKey("auth.website_type") Purpose: Determines registration behavior: - Value "2": Invite-only registration. Checks if user email exists in invite table. - Any other value: Open registration allowed. Called in: registerUser() function (lines 61, 119 across main and refactored versions)

3.2 Registration Policy (RegistrationPolicy.ts)

Setting consumed: auth.website_type How: SettingService.getSettingByKey("auth.website_type") Purpose: Policy check before registration. If value is "2", verifies user is in the invite list. Called in: can() method of RegisterPolicy

3.3 Stripe Service (stripe.service.ts)

Config consumed: stripe_secret_key (AppConfig, NOT Setting) How: AppConfigService.getConfigByKey("stripe_secret_key") Purpose: Retrieves the Stripe secret key at runtime from the database instead of environment variables. Called in: Line 229 of stripe.service.ts

3.4 Image Size (Dead Code)

Setting referenced: admin.image_size How: SettingService.getImageSizeByKey() in setting.service.ts Purpose: Would return max image upload size in MB. Status: DEAD CODE -- defined in the service but never called by any controller, route, or other service.


4. FRONTEND API HOOKS LAYER

4.1 Authenticated Settings Hook (src/api/settings/index.js)

useSettings() -- Fetch ALL settings (authenticated)

const useSettings = () => {
  return useQuery({
    queryKey: ["settings"],
    queryFn: async () => {
      const res = await api.get("/settings");  // GET /api/v1/settings
      const formattedSettings = res?.data?.data?.reduce((acc, item) => {
        const [group, field] = item.key.split(".");
        if (!acc[group]) acc[group] = {};
        acc[group][field] = item.value;
        return acc;
      }, {});
      return { settings: formattedSettings, data: res?.data?.data || [] };
    },
    staleTime: (parseInt(process.env.NEXT_PUBLIC_CACHE_TIME) || 1) * 60 * 60 * 1000,
  });
};

Data transformation: Raw array [{key: "site.footer_text", value: "..."}] is transformed into nested object { site: { footer_text: "..." }, admin: { ... } }.

Returns two shapes: settings (nested by group) and data (raw array). The settings shape goes to Redux; the data shape is used by the site-settings admin page.

Stale time: Configurable via NEXT_PUBLIC_CACHE_TIME env var (default: 1 hour in milliseconds).

useSettingByGroup(group) -- Fetch settings by group prefix (authenticated)

const useSettingByGroup = (group) => {
  return useQuery({
    queryKey: ["settings", group],
    queryFn: async () => {
      const res = await api.get(`/settings/${group}/prefix`);
      // Same dot-split transformation...
      return settings;  // Nested object: { site: { footer_text: "..." } }
    },
    staleTime: ...,
  });
};

Used by: sidebar-old.jsx only (fetches "site" group directly via authenticated API).

useUpdateSetting(), useCreateSetting(), useDeleteSetting() -- Mutation hooks

All invalidate the ["settings"] query key on success. useUpdateSetting and useCreateSetting also call revalidate("settings-group") to bust the Next.js server-side cache.

4.2 Public Settings (Server-Side) (src/api/public/settings.js)

export const getSettingByGroup = unstable_cache(
  async (group) => {
    const response = await fetch(`${process.env.API_URL}/settings/${group}/prefix`);
    const data = await response.json();
    const settings = data?.data?.reduce(/* same dot-split transform */);
    return settings;
  },
  ["settings-group"],        // Cache key
  {
    revalidate: (parseInt(process.env.NEXT_PUBLIC_CACHE_TIME) || 1) * 60 * 60,
    tags: ["settings-group"], // Revalidation tag
  }
);

Key difference from authenticated hook: Uses fetch() directly (not axios), uses process.env.API_URL (server-side env var), and wraps in Next.js unstable_cache for ISR/SSR caching.

Cache invalidation: When a setting is updated via useUpdateSetting or useCreateSetting, they call revalidate("settings-group") which triggers revalidateTag("settings-group") server action, busting this cache.

4.3 Account Settings (src/api/settings/account-setting.js)

NOT part of the Setting entity. This is the UserSetting entity (per-user 2FA configuration). Separate system: - useAccountSettings() -- GET /user-settings/me - useSetupTwoFactor() -- PUT /user-settings/2fa/google/setup - useVerifyOTP() -- Various 2FA verification endpoints - useBackupCodes() -- POST /user-settings/2fa/generate-backup-codes

These are user-scoped settings, not application-wide settings.


5. REDUX STORE LAYER

5.1 Store Configuration (src/store/index.js)

export const store = configureStore({
  reducer: {
    auth: authReducer,
    setting: settinReducer,  // NOTE: typo "settinReducer" (missing 'g')
  },
});

5.2 Setting Slice (src/store/slices/settingSlice.js)

const initialState = { settings: {} };

const settingSlice = createSlice({
  name: "setting",
  reducers: {
    setSettings: (state, action) => {
      state.settings = action.payload;
    },
  },
});

export const { setSettings } = settingSlice.actions;

const EMPTY_OBJECT = {};
export const getAllSettings = (state) => state.setting.settings;
export const getSettingByGroup = (group) => (state) =>
  state.setting.settings[group] ?? EMPTY_OBJECT;

State shape: { setting: { settings: { site: { footer_text: "..." }, admin: { site_name: "..." }, auth: { ... }, contact: { ... } } } }

IMPORTANT: getSettingByGroup returns EMPTY_OBJECT (a stable reference) when the group doesn't exist, preventing unnecessary re-renders. This is a good pattern.

5.3 How Settings Enter Redux

The ONLY entry point is dashboard/layout.js:

const { data: settings } = useSettings();   // Fetch from API
const dispatch = useDispatch();

useEffect(() => {
  if (settings?.settings) {
    dispatch(setSettings(settings.settings)); // Push nested object to Redux
  }
}, [settings, dispatch]);

This means: 1. Settings are ONLY loaded into Redux when the dashboard layout mounts. 2. Public pages (main layout) do NOT populate Redux with settings. 3. Redux settings are only available in the dashboard context.


6. SSR SETTINGS ACCESS (SERVER COMPONENTS)

Server components use getSettingByGroup() from @/api/public/settings. This function: 1. Calls the backend API at build/request time using fetch() 2. Uses unstable_cache with a revalidation period 3. Returns the nested object format

6.1 SSR Consumer: Root Layout (src/app/layout.js)

Function: fetchMetaData() (NOT using the shared getSettingByGroup) API Call: fetch(\${process.env.API_URL}/settings/admin/prefix`)**Caching**: Usesnext: { revalidate: ... }(Next.js fetch cache, NOTunstable_cache) **Fields accessed**: -settings?.admin?.meta_title-- Page title, fallback:"Sass Boilerplate"-settings?.admin?.site_name-- Title template suffix -settings?.admin?.meta_description-- SEO description, fallback:"SaaS Boilerplate"-settings?.admin?.favicon-- Favicon URL -settings?.admin?.meta_image` -- Open Graph / Twitter card image

Error handling: Catches errors and returns the string "SASS Boilerplate" (not an object). This will cause settings?.admin?.meta_title to be undefined, triggering the fallback.

6.2 SSR Consumer: Login Page (src/app/(auth)/login/page.js)

API Calls: - getSettingByGroup("auth") -- for page metadata and auth settings - getSettingByGroup("site") -- for site logo

Fields accessed (metadata): - settings?.auth?.login_page_title -- Page title, fallback: "Login"

Props passed to LoginPage component: - settings?.auth as settings - siteSettings?.site as siteSettings

6.3 SSR Consumer: Register Page (src/app/(auth)/register/page.js)

API Calls: - getSettingByGroup("auth") -- for page metadata and auth settings - getSettingByGroup("site") -- for site logo

Fields accessed (metadata): - settings?.auth?.register_page_title -- Page title, fallback: "Register"

Props passed to Register component: - settings?.auth as settings - siteSettings?.site as siteSettings

6.4 SSR Consumer: Contact Page (src/app/(main-layout)/contact/page.js)

API Call: getSettingByGroup("contact") Props passed to ContactPage component: settings?.contact as settings

6.5 SSR Consumer: Header Wrapper (src/components/layout/header-wrapper.jsx)

API Call: getSettingByGroup("site") Props passed to Header component: settings?.site as settings

API Call: getSettingByGroup("site") Props passed to Footer component: settings?.site as settings


7. CSR SETTINGS ACCESS (CLIENT COMPONENTS)

Client components access settings in two ways: 1. Via Redux (populated by dashboard layout) using useSelector(getSettingByGroup("group")) 2. Via React Query hooks directly (useSettings(), useSettingByGroup()) 3. Via props (passed from SSR server components)

7.1 Dashboard Layout as Settings Gateway

src/app/(dashboard-layout)/dashboard/layout.js is the central hub for dashboard settings:

useSettings() API call
    |
    v
useEffect -> dispatch(setSettings(settings.settings)) -> Redux Store
    |
    v
useSelector(getSettingByGroup("site"))   -> passed as prop to Sidebar
useSelector(getSettingByGroup("admin"))  -> used for <title> tag

Fields accessed directly in layout: - adminSettings?.admin_site_title -- Dashboard page title (NOT SEEDED) - adminSettings?.site_name -- Dashboard page title suffix (SEEDED as admin.site_name) - siteSettings -- passed entirely to Sidebar component


8. COMPLETE CONSUMER COMPONENT REGISTRY

Aspect Detail
File src/components/layout/footer-credit.jsx
Access method useSelector(getSettingByGroup("site")) (Redux)
Fields accessed footer_site_link, footer_site_name, footer_copyright, footer_heart_link
Fallbacks footer_site_link -> "https://algorizetech.com", footer_site_name -> "Algorize Tech", footer_heart_link -> "https://algorizetech.com", footer_copyright -> NO FALLBACK (renders empty)
Data flow API -> useSettings() -> Redux -> useSelector
Context Only renders inside dashboard layout (after Redux is populated)
Used for Copyright text, credit links in dashboard footer

CONSUMER 2: dashboard/layout.js

Aspect Detail
File src/app/(dashboard-layout)/dashboard/layout.js
Access method useSettings() hook + Redux dispatch + useSelector()
Fields accessed adminSettings?.admin_site_title, adminSettings?.site_name (from "admin" group), entire siteSettings object (from "site" group)
Fallbacks None for title fields. If settings API is loading, shows <Loading fullScreen />
Data flow API -> useSettings() -> dispatch to Redux AND useSelector
Used for <title> tag, passing siteSettings to Sidebar, passing isSettingsLoading to Sidebar

CONSUMER 3: top-nav.jsx

Aspect Detail
File src/app/(dashboard-layout)/dashboard/components/top-nav.jsx
Access method NONE -- does NOT consume settings
Note Has a commented-out import: // import { useSettings } from "@/contexts/settings-context". Settings context was planned but never implemented.
Used for Breadcrumb navigation, user dropdown menu (uses user data, not settings)

CONSUMER 4: sidebar-old.jsx (DEPRECATED)

Aspect Detail
File src/app/(dashboard-layout)/dashboard/components/sidebar-old.jsx
Access method useSettingByGroup("site") (direct React Query hook, NOT Redux)
Fields accessed siteSettings?.site?.site_logo, siteSettings?.site?.site_logo_dark
Fallbacks Falls back to "/logo.png" for dark logo if missing
Data flow API -> useSettingByGroup("site") -> local query state
Used for Sidebar logo display (light/dark theme variants)
Note This is the OLD sidebar. Has a commented-out reference to siteSettings?.admin?.site_name

CONSUMER 5: sidebar.jsx (CURRENT)

Aspect Detail
File src/app/(dashboard-layout)/dashboard/components/sidebar.jsx
Access method Receives siteSettings as prop from dashboard layout
Fields accessed siteSettings?.site_logo, siteSettings?.site_logo_dark, siteSettings?.site_name
Fallbacks If site_logo is falsy or equals "undefined", displays siteSettings?.site_name as text, with final fallback "Dashboard". Dark logo falls back to "/logo.png"
Data flow API -> useSettings() -> Redux -> layout useSelector -> prop drill -> Sidebar
Used for Sidebar header logo (light/dark), fallback text when no logo

CONSUMER 6: site-settings-page.jsx

Aspect Detail
File src/components/pages/settings/site-settings-page.jsx
Access method useSettings() (React Query hook)
Fields accessed settings?.data (raw array), iterates ALL settings
Fallbacks Shows "No Settings Found" message if groups are empty. Shows <Loading semiScreen /> while loading
Data flow API -> useSettings() -> local state
Used for Admin settings management UI. Groups settings by group field, renders editable SettingField per setting. Sorts by order.
Permission Wrapped in <PermissionWrapper permission="setting.view">

CONSUMER 7: setting-field.jsx

Aspect Detail
File src/components/pages/settings/setting-field.jsx
Access method Receives individual setting object as prop from site-settings-page
Fields accessed setting.id, setting.value, setting.type, setting.display_name, setting.key, setting.details
Data flow API -> useSettings() -> site-settings-page -> prop -> setting-field
Used for Individual setting editor. Renders appropriate input type (text/textarea/select/image/checkbox). Updates via useUpdateSetting(), deletes via useDeleteSetting()
Permissions Update wrapped in <PermissionWrapper permission="setting.update">, delete in <PermissionWrapper permission="setting.delete">

CONSUMER 8: create-setting-form.jsx

Aspect Detail
File src/components/pages/settings/create-setting-form.jsx
Access method Creates/updates settings via useCreateSetting() and useUpdateSetting()
Fields accessed In edit mode: setting.key, setting.display_name, setting.type, setting.order, setting.group, setting.value, setting.details
Data flow Props from setting-field (edit mode) or standalone (create mode)
Used for Modal form for creating new settings or editing existing ones
Permission Create wrapped in <PermissionWrapper permission="setting.create">

CONSUMER 9: header.jsx (Public Site Header)

Aspect Detail
File src/components/layout/header.jsx
Access method Receives settings as prop from SSR HeaderWrapper
Fields accessed settings?.site_logo, settings?.site_logo_dark
Fallbacks None. If logos are undefined, <Image> will have undefined src
Data flow SSR: getSettingByGroup("site") -> HeaderWrapper -> prop -> Header
Used for Public site header logo (light/dark variants)
Aspect Detail
File src/components/layout/footer.jsx
Access method Receives settings as prop from SSR FooterWrapper
Fields accessed settings?.site_logo, settings?.site_logo_dark, settings?.footer_text, settings?.footer_copyright
Fallbacks None. All fields render as empty if missing
Data flow SSR: getSettingByGroup("site") -> FooterWrapper -> prop -> Footer
Used for Public site footer: logo, description text, copyright

CONSUMER 11: login.jsx (Login Page Component)

Aspect Detail
File src/components/pages/auth/login.jsx
Access method Receives settings and siteSettings as props from SSR login page
Fields accessed from settings (auth group) login_page_type, login_image
Fields accessed from siteSettings (site group) site_logo, site_logo_dark
Fallbacks login_page_type -> "1", login_image -> imported loginImg static asset
Data flow SSR: getSettingByGroup("auth") + getSettingByGroup("site") -> page.js -> props -> LoginPage
Used for Controls login page layout type (1=default with bg, 2=custom image, 3=form only), custom background image, site logo

CONSUMER 12: register.jsx (Register Page Component)

Aspect Detail
File src/components/pages/auth/register.jsx
Access method Receives settings and siteSettings as props from SSR register page
Fields accessed from settings (auth group) website_type, register_page_type, register_image
Fields accessed from siteSettings (site group) site_logo, site_logo_dark
Fallbacks website_type -> 1, register_page_type -> "1"
Data flow SSR: getSettingByGroup("auth") + getSettingByGroup("site") -> page.js -> props -> Register
Used for Controls registration behavior (open/invite-only/approval), page layout type, custom background image, site logo. website_type controls invite-check logic

CONSUMER 13: contact-page.jsx (Contact Page Component)

Aspect Detail
File src/components/pages/contact-frontend/contact-page.jsx
Access method Receives settings as prop from SSR contact page
Fields accessed page_title, page_desc, contact_phone, contact_email, contact_address
Fallbacks None. All fields render empty if missing
Data flow SSR: getSettingByGroup("contact") -> page.js -> props -> ContactPage
Used for Contact page: title, description, phone number, email, physical address

CONSUMER 14: Root Layout generateMetadata() (SSR)

Aspect Detail
File src/app/layout.js
Access method Direct fetch() to /settings/admin/prefix (NOT using shared getSettingByGroup)
Fields accessed admin.meta_title, admin.site_name, admin.meta_description, admin.favicon, admin.meta_image
Fallbacks meta_title -> "Sass Boilerplate", meta_description -> "SaaS Boilerplate"
Data flow SSR: fetch() -> transform -> generateMetadata return
Used for HTML <title>, meta description, favicon, Open Graph tags, Twitter cards

9. COMPLETE SETTING KEY REGISTRY

Keys consumed by FRONTEND (all access paths):

Full Key Group Field Where Consumed Access Path
admin.site_name admin site_name Root layout (title template), Dashboard layout SSR fetch + Redux
admin.meta_title admin meta_title Root layout generateMetadata SSR fetch
admin.meta_description admin meta_description Root layout generateMetadata SSR fetch
admin.favicon admin favicon Root layout generateMetadata SSR fetch
admin.meta_image admin meta_image Root layout generateMetadata SSR fetch
admin.admin_site_title admin admin_site_title Dashboard layout <title> Redux
site.site_logo site site_logo Header, Footer, Sidebar, Sidebar-old, Login, Register SSR props + Redux prop drill
site.site_logo_dark site site_logo_dark Header, Footer, Sidebar, Sidebar-old, Login, Register SSR props + Redux prop drill
site.footer_text site footer_text Footer (public) SSR props
site.footer_copyright site footer_copyright Footer (public), FooterCredit (dashboard) SSR props + Redux
site.footer_site_link site footer_site_link FooterCredit (dashboard) Redux
site.footer_site_name site footer_site_name FooterCredit (dashboard) Redux
site.footer_heart_link site footer_heart_link FooterCredit (dashboard) Redux
site.site_name site site_name Sidebar (current) -- fallback text Redux prop drill
auth.login_page_type auth login_page_type Login page, Login page metadata SSR props
auth.login_image auth login_image Login page SSR props
auth.register_page_type auth register_page_type Register page SSR props
auth.register_image auth register_image Register page SSR props
auth.website_type auth website_type Register page (invite-only logic) SSR props
auth.login_page_title auth login_page_title Login page generateMetadata SSR
auth.register_page_title auth register_page_title Register page generateMetadata SSR
contact.page_title contact page_title Contact page SSR props
contact.page_desc contact page_desc Contact page SSR props
contact.contact_phone contact contact_phone Contact page SSR props
contact.contact_email contact contact_email Contact page SSR props
contact.contact_address contact contact_address Contact page SSR props

Keys consumed by BACKEND:

Full Key Where Consumed Purpose
auth.website_type auth.service.ts, RegistrationPolicy.ts Invite-only registration check
admin.image_size setting.service.ts (DEAD CODE) Max image upload size
stripe_secret_key stripe.service.ts (AppConfig) Stripe API authentication

10. SETTINGS DATA FLOW MAP

                                    ┌─────────────────────────────┐
                                    │   PostgreSQL "settings"     │
                                    │   table (Setting entity)    │
                                    │   17 seeded rows            │
                                    └─────────────┬───────────────┘
                              ┌───────────────────┼───────────────────┐
                              │                   │                   │
                     SettingService        SettingService       SettingService
                    .getAllSettings()   .getSettingsByPrefix() .getSettingByKey()
                              │                   │                   │
                              │                   │                   │
                    SettingController     SettingController      (Internal only)
                    .getAllSettings       .getSettingByPrefix         │
                              │                   │                   │
                    ┌─────────┴──────┐   ┌────────┴───────┐    ┌─────┴──────────┐
                    │ GET /settings  │   │ GET /settings/ │    │ auth.service   │
                    │ (NO AUTH!)     │   │ :prefix/prefix │    │ Registration   │
                    │                │   │ (NO AUTH!)     │    │ Policy         │
                    └───────┬────────┘   └───────┬────────┘    └────────────────┘
                            │                    │
              ┌─────────────┘                    └──────────────────┐
              │                                                     │
    ┌─────────┴──────────┐                           ┌──────────────┴────────────────┐
    │   FRONTEND CSR     │                           │     FRONTEND SSR              │
    │   (Dashboard)      │                           │   (Server Components)         │
    │                    │                           │                               │
    │  useSettings()     │                           │  getSettingByGroup(group)      │
    │  queryKey:         │                           │  unstable_cache                │
    │  ["settings"]      │                           │  tags: ["settings-group"]      │
    │  GET /settings     │                           │  fetch() /settings/:grp/prefix │
    └────────┬───────────┘                           └──────────────┬────────────────┘
             │                                                      │
    ┌────────┴───────────┐                           ┌──────────────┴────────────────┐
    │  Transform:        │                           │  Transform:                   │
    │  [{key:"a.b",      │                           │  Same dot-split               │
    │    value:"x"}]     │                           │  -> { group: { field: val } } │
    │  -> { a: {b:"x"} } │                          └──────────────┬────────────────┘
    └────────┬───────────┘                                          │
             │                                          ┌───────────┴───────────────┐
    ┌────────┴───────────┐                              │                           │
    │  Redux Dispatch    │                    ┌─────────┴──────────┐    ┌───────────┴──────────┐
    │  setSettings(      │                    │  layout.js         │    │  page.js files       │
    │    {settings}      │                    │  fetchMetaData()   │    │  (login, register,   │
    │  )                 │                    │  generateMetadata  │    │   contact)           │
    └────────┬───────────┘                    │  -> <meta> tags    │    │  generateMetadata    │
             │                                └────────────────────┘    │  -> page title       │
    ┌────────┴───────────┐                                              │  -> props to client  │
    │  Redux Store       │                                              │     components       │
    │  state.setting     │                                              └──────────┬───────────┘
    │  .settings         │                                                         │
    │  { site: {...},    │                                              ┌──────────┴───────────┐
    │    admin: {...},   │                                              │  Client Components   │
    │    auth: {...},    │                                              │  via Props:          │
    │    contact: {...}} │                                              │  - LoginPage         │
    └────────┬───────────┘                                              │  - Register          │
             │                                                          │  - ContactPage       │
    ┌────────┴────────────────────────┐                                 │  - Header            │
    │  useSelector(                   │                                 │  - Footer            │
    │    getSettingByGroup("group")   │                                 └──────────────────────┘
    │  )                              │
    │                                 │
    │  Dashboard consumers:           │
    │  - layout.js (admin, site)      │
    │  - footer-credit.jsx (site)     │
    │  - sidebar.jsx (site via prop)  │
    └─────────────────────────────────┘

Dual Access Path for "site" Group (Dashboard Context)

The "site" settings traverse TWO paths in the dashboard:

Path A (Redux -> direct selector):
  useSettings() -> Redux -> useSelector(getSettingByGroup("site")) -> footer-credit.jsx

Path B (Redux -> selector -> prop drill):
  useSettings() -> Redux -> useSelector(getSettingByGroup("site")) -> layout.js
    -> <Sidebar siteSettings={siteSettings} /> -> sidebar.jsx

Dual Access Path for "site" Group (Old Sidebar)

sidebar-old.jsx uses BOTH:
  Path A: useSettingByGroup("site") -> direct API call (authenticated, separate cache)
  Path B: The old sidebar is NOT used in the current layout, so this is dead code

11. CACHING STRATEGY ANALYSIS

11.1 Three Caching Layers

Layer Mechanism Duration Scope Invalidation
Next.js fetch cache next: { revalidate } NEXT_PUBLIC_CACHE_TIME hours (default: 1h) Per-fetch, server-side Time-based only
Next.js unstable_cache unstable_cache() NEXT_PUBLIC_CACHE_TIME hours (default: 1h) Shared across requests revalidateTag("settings-group") + time-based
React Query cache staleTime NEXT_PUBLIC_CACHE_TIME hours (default: 1h) Per-client, client-side invalidateQueries(["settings"]) on mutation

11.2 Cache Invalidation Flow

When an admin updates a setting:

useUpdateSetting.mutate({id, value})
  |
  ├── onSuccess:
  │   ├── queryClient.invalidateQueries({ queryKey: ["settings"] })
  │   │   -> React Query refetches useSettings() on next component mount
  │   │   -> BUT does NOT invalidate useSettingByGroup() queries (different key!)
  │   │
  │   └── await revalidate("settings-group")
  │       -> Server action calls revalidateTag("settings-group")
  │       -> Busts unstable_cache for SSR pages
  │       -> Does NOT bust the fetch cache in root layout (uses different mechanism)

ISSUE: The root layout's fetchMetaData() uses next: { revalidate } which is time-based only. Admin settings updates (meta_title, favicon, etc.) will NOT be reflected until the time-based cache expires. The revalidateTag mechanism does not reach this cache.

11.3 Environment-Dependent Caching

Variable Used For Default
NEXT_PUBLIC_CACHE_TIME All three caching layers (hours) 1 (hour)
API_URL Server-side fetch (SSR) Must be set
NEXT_PUBLIC_API_URL Client-side API calls via axios Must be set

No dev vs. production differentiation in caching strategy. The stale time is the same in all environments.


12. ERROR STATES & FALLBACK ANALYSIS

12.1 If Settings API Is Down

Context Behavior
Root layout SSR Catches error, returns "SASS Boilerplate" string. Fallbacks activate for meta tags.
Public getSettingByGroup SSR Catches error, returns [] (empty array). All setting fields will be undefined.
Dashboard layout CSR useSettings() throws, isSettingsLoading stays truthy. Infinite loading screen -- the layout renders <Loading fullScreen /> until settings load. No timeout or error UI.
sidebar-old.jsx CSR useSettingByGroup() throws. isSettingsLoading stays true. Logo section stays hidden (guarded by !isSettingsLoading).
Login/Register SSR getSettingByGroup() returns []. Props will be undefined. Components use hardcoded fallbacks for page type.
Contact page SSR getSettingByGroup() returns []. Props will be undefined. Contact info renders empty.

12.2 Fallback Values Summary

Field Component Fallback
footer_site_link footer-credit.jsx "https://algorizetech.com"
footer_site_name footer-credit.jsx "Algorize Tech"
footer_heart_link footer-credit.jsx "https://algorizetech.com"
footer_copyright footer-credit.jsx NONE (renders empty)
login_page_type login.jsx "1"
login_image login.jsx Imported loginImg static asset
register_page_type register.jsx "1"
website_type register.jsx 1 (number, not string!)
site_logo sidebar.jsx Falls through to site_name, then "Dashboard"
site_logo_dark sidebar.jsx "/logo.png"
site_logo_dark sidebar-old.jsx "/logo.png"
meta_title layout.js "Sass Boilerplate"
meta_description layout.js "SaaS Boilerplate"
login_page_title login page.js "Login"
register_page_title register page.js "Register"
All site fields header.jsx NONE
All site fields footer.jsx NONE
All contact fields contact-page.jsx NONE

13. PUBLIC VS AUTHENTICATED ACCESS PATTERNS

13.1 Public Access (No Auth Required)

All three GET endpoints on /settings are public (no auth middleware): - GET /settings -- returns ALL settings - GET /settings/:prefix/prefix -- returns settings by group prefix - GET /settings/:id -- returns single setting by ID

Frontend public consumers (SSR, using process.env.API_URL): - Root layout: fetches admin group - Login page: fetches auth and site groups - Register page: fetches auth and site groups - Contact page: fetches contact group - Header wrapper: fetches site group - Footer wrapper: fetches site group

13.2 Authenticated Access

Write operations require auth + permission checks: - POST /settings -- requires setting.create permission - PATCH /settings/:id -- requires setting.update permission - PATCH /settings/bulk-update -- requires setting.bulkUpdate permission - DELETE /settings/:id -- requires setting.delete permission

Frontend authenticated consumers (CSR, using axios with auth interceptor): - Dashboard layout: useSettings() -- fetches ALL settings via GET /settings - Site settings page: useSettings() -- fetches ALL settings - sidebar-old.jsx: useSettingByGroup("site") -- fetches site group

13.3 Security Observation

The GET /settings endpoint returns ALL settings including their full key, value, type, details, group, and order. There is no filtering of sensitive data. While current seed data contains only UI configuration, any setting added to this table (including potentially sensitive ones) would be publicly accessible.


14. APPCONFIG: SEPARATE SYSTEM

AppConfig is a completely separate system from Settings:

Aspect Setting AppConfig
Purpose UI configuration, site branding Server-side secrets (API keys)
Entity Setting (settings table) AppConfig (app_configs table)
Frontend access Yes (public GET, authenticated CRUD) NO (only backend)
Encryption No Yes (isEncrypted flag, encrypt() function)
Key format group.field_name Simple key (e.g., stripe_secret_key)
Only backend consumer auth.service.ts, RegistrationPolicy stripe.service.ts
Route /api/v1/settings /api/v1/app-configs
Seed data 17 rows None
Permission setting.* app_config.* / create_app_config.* / update_app_config.*

Note: AppConfig has inconsistent permission strings: app_config.read, create_app_config.create, update_app_config.update, app_config.delete. The create and update permissions use different prefixes than read and delete.


15. ISSUES FOUND

CRITICAL Issues

# Issue Severity Details
C1 Dashboard infinite loading if settings API fails Critical dashboard/layout.js line 39: if (!user \|\| isLoading \|\| bottomLinksLoading \|\| isSettingsLoading) return <Loading fullScreen />. If the settings API is unreachable, the entire dashboard is permanently blocked with no error message, no timeout, and no retry UI.
C2 All settings publicly readable without authentication Critical setting.routes.ts places GET routes BEFORE router.use(auth()). All 17+ settings (and any future sensitive settings) are exposed via GET /settings.
C3 admin.admin_site_title key consumed but NEVER seeded or defined Critical dashboard/layout.js line 46 reads adminSettings?.admin_site_title. This key does not exist in seed data, the key naming convention would require the full key to be admin.admin_site_title, and the dot-split transform would produce { admin: { admin_site_title: "..." } }. Since admin.admin_site_title is not seeded, the dashboard <title> tag renders undefined | undefined.
C4 admin.meta_title key consumed but NOT seeded Critical Root layout generateMetadata() reads settings?.admin?.meta_title but this key is not in the seed data. Fallback "Sass Boilerplate" activates. The setting must be manually created by an admin.

HIGH Issues

# Issue Severity Details
H1 Root layout fetch cache not invalidated on setting update High layout.js uses next: { revalidate } (time-based), NOT unstable_cache with tags. When admin updates meta_title/favicon via the settings UI, the revalidateTag("settings-group") call does NOT bust this cache. Changes only appear after the time-based revalidation period expires.
H2 Duplicate settings fetch in root layout High layout.js has its own fetchMetaData() that duplicates the exact same transform logic as getSettingByGroup() in api/public/settings.js. Different caching mechanism (fetch cache vs unstable_cache). Should use the shared function.
H3 useSettingByGroup() cache NOT invalidated on mutation High useUpdateSetting.onSuccess invalidates ["settings"] but NOT ["settings", group]. The sidebar-old.jsx (which uses useSettingByGroup("site") with queryKey ["settings", "site"]) would NOT get the updated data until its stale time expires.
H4 5 setting keys consumed by frontend but never seeded High Keys admin.admin_site_title, admin.meta_title, site.footer_site_link, site.footer_site_name, site.footer_heart_link, auth.login_page_title, auth.register_page_title are all consumed but not in seed data. Requires manual creation by admin.
H5 backupCodes column on Setting entity is misplaced High The Setting entity has backupCodes: string[] and verifyBackupCode() method references a userId column that does NOT exist on Setting. This is clearly UserSetting functionality incorrectly placed on the Setting entity. The verifyBackupCode() method in setting.service.ts would crash with Property 'userId' does not exist on type 'FindOptionsWhere<Setting>'.

MEDIUM Issues

# Issue Severity Details
M1 Store reducer typo: settinReducer Medium src/store/index.js line 3: import settinReducer from "./slices/settingSlice". Missing 'g'. Functionally correct but confusing.
M2 Error log message mismatch in public settings Medium api/public/settings.js line 16: console.log("Failed to fetch navigation links") -- should say "settings", not "navigation links". Copy-paste error from navigation-links module.
M3 website_type type inconsistency between frontend and backend Medium register.jsx line 52: const websiteType = settings?.website_type \|\| 1 (number fallback). But seed data has value: "2" (string). Comparison websiteType == 2 uses loose equality which works, but websiteType === 2 would fail. Backend checks setting.value === "2" (strict string).
M4 sidebar-old.jsx is orphaned/dead code Medium The current dashboard/layout.js imports sidebar.jsx (not sidebar-old.jsx). The old sidebar has a completely different settings access pattern (direct API hook vs prop drill) and is never rendered.
M5 Seed data type inconsistency: "input" vs "text" Medium Seed data uses type: "input" for text fields, but create-setting-form.jsx SETTING_TYPES lists "text" (not "input"). The setting-field.jsx switch/case handles "text" and "textarea" but NOT "input" -- seeded settings with type "input" will fall through to the default case which happens to render a plain Input. Works by accident.
M6 getSettingByGroup name collision: two different functions with same name Medium getSettingByGroup exists in BOTH @/api/public/settings (SSR, uses unstable_cache) AND @/store/slices/settingSlice (Redux selector). Different signatures, different purposes, same export name. Imports must be carefully traced.
M7 AppConfig permission strings inconsistent Medium Read uses app_config.read, but create uses create_app_config.create and update uses update_app_config.update. The verb-prefixed pattern breaks the standard entity.action convention.
M8 deleteAppConfig uses hard delete, not soft delete Medium AppConfigService.deleteAppConfig() calls configRepo.delete(id) (hard delete) despite the entity having @DeleteDateColumn(). All other entities use soft delete.

LOW Issues

# Issue Severity Details
L1 No <Image> fallback in header.jsx Low If settings?.site_logo is undefined, Next.js <Image> will receive src={undefined} which may cause a runtime warning or broken image.
L2 footer_copyright no fallback in footer-credit.jsx Low All other footer-credit fields have fallbacks except footer_copyright which renders {siteSettings?.footer_copyright} with no fallback (empty space).
L3 keepPreviousData is deprecated in React Query v5 Low useSettings() and useSettingByGroup() both use keepPreviousData: true which is deprecated in TanStack Query v5 (should use placeholderData: keepPreviousData).
L4 getImageSizeByKey() is dead code Low Defined in setting.service.ts but never called anywhere.
L5 Settings error console.error says "navigation links" Low api/public/settings.js line 28: console.error("Error fetching navigation links:", error) should say "settings".
L6 updateSettingById controller returns "Setting created" message Low setting.controller.ts line 79: message: "Setting created successfully" on the update endpoint. Should say "updated".
L7 Login page uses window.location.href instead of Next.js router Low Multiple locations in login.jsx use window.location.href = "/dashboard" for navigation after login, causing full page reload instead of client-side navigation. Not directly a settings issue but part of the settings-consuming component.
L8 unstable_cache API may change Low The SSR settings access relies on unstable_cache from next/cache, which is labeled as unstable and may change in future Next.js versions.

SUMMARY

Architecture Quality Assessment

Strengths: 1. Clean separation between public (Setting) and private (AppConfig) configuration 2. EMPTY_OBJECT pattern in Redux selector prevents unnecessary re-renders 3. Consistent dot-notation key format enables automatic grouping 4. SSR settings access for public pages (login, register, contact, header, footer) 5. Cache invalidation chain from client mutations to server cache via revalidateTag

Weaknesses: 1. Dual settings access patterns (Redux vs direct API hooks) create confusion 2. Settings are a blocking dependency for the entire dashboard (infinite loading on failure) 3. Five+ settings keys are consumed but never seeded 4. Root layout uses a separate fetch mechanism that bypasses the shared invalidation 5. No authentication on GET settings endpoints 6. backupCodes and verifyBackupCode are misplaced on the Setting entity 7. Seed data type ("input") doesn't match the create form's type options ("text")

Issue Count

Severity Count
Critical 4
High 5
Medium 8
Low 8
Total 25