Skip to content

Settings & AppConfig Sprint Plan — Cross-Functional War Room Output

Context: 25 issues from Settings & AppConfig Deep-Dive Generated: 2026-04-12 | Method: Cross-Functional War Room (PM + Engineer + Designer) Participants: PM (business impact), Engineer (effort/risk), Designer (UX impact) Related: Deep-Dive | Settings & Configuration Sprint Plan Scope Note: This plan covers the Setting entity (UI configuration) and AppConfig entity (server-side secrets) data flow analysis. It is complementary to — but distinct from — the Settings & Configuration sprint plan, which covered the broader UserSetting / 2FA / validation surface. Where overlap exists (e.g. AppConfig permission naming, hard-delete fixes), this plan defers to whichever bucket lands the fix first.

Frontend Progress Update (2026-04-13): All frontend items verified against Sass-boilerplate-frontend/. ✅ Resolved: Ship #5 (C1), S1-1, S1-2, S1-3, S1-4, S1-5, S1-6, S1-7, S1-8, S2-1, S2-2, S2-3, S2-4, S2-5, S2-11 (closed as won't-fix after architectural review — see row for details). Backend items (C2, C3, C4, H4, M7, M8, H5, S2-7, S2-8) and documentation/test items (S2-9, S2-10, S2-12) remain open.


Triage Summary

Bucket Criteria Count Effort
Ship Blocker Cannot deploy. Security exploit, data loss, broken core surface. 8 ~50 min
Sprint 1 Settings caching/state works end-to-end. Type & cache consistency. 8 ~4 hrs
Sprint 2 Code quality, dead code, fallbacks, docs, integration tests. 8 ~6 hrs
Backlog Cosmetic, deferred, or future-Next.js migration. 1 Ongoing

Issue Count by Severity (from deep-dive):

Severity Count IDs
Critical 4 C1, C2, C3, C4
High 5 H1, H2, H3, H4, H5
Medium 8 M1, M2, M3, M4, M5, M6, M7, M8
Low 8 L1, L2, L3, L4, L5, L6, L7, L8
Total 25

Ship Blockers (Pre-Release Gate)

Timeline: ~50 minutes, 1 developer Exit criteria: Settings GET endpoints authenticated. Dashboard survives a settings outage. All consumed setting keys exist in seed. AppConfig CRUD reachable for non-super_admin. No data loss on AppConfig delete.

Order Issue Description File Change Effort
1 C2 Authenticate GET /settings/* routes setting.routes.ts:109-121 Move all 3 GET registrations (/, /:prefix/prefix, /:id) to AFTER router.use(auth(), contextMiddleware). If public access is genuinely required for SSR pages, mount a separate publicSettingRouter with an explicit allow-list of group prefixes (e.g. site, admin, auth, contact) only. 10 min
2 C3 / H4 Seed admin.admin_site_title seed/data/setting.ts Add row: { group: "admin", key: "admin.admin_site_title", value: "SaaS Boilerplate", display_name: "Dashboard Title", type: "text", order: 6 } 2 min
3 C4 / H4 Seed admin.meta_title seed/data/setting.ts Add row: { group: "admin", key: "admin.meta_title", value: "SaaS Boilerplate", display_name: "Page Meta Title", type: "text", order: 7 } 2 min
4 H4 Seed remaining footer + auth keys seed/data/setting.ts Add 5 rows: site.footer_site_link, site.footer_site_name, site.footer_heart_link, auth.login_page_title, auth.register_page_title with sensible defaults matching the in-code fallbacks (https://yoursite.com, Your Site Name, Login, Register). 5 min
5 C1 RESOLVED 2026-04-13 — Stop blocking dashboard on settings load dashboard/layout.js:39 Remove isSettingsLoading from the gate. Render the dashboard with whatever settings have arrived; let the title fall through to the existing ?? "Dashboard" fallback. Show a one-time react-hot-toast error if useSettings() enters error state. (Mirrors the FE-M7 fix already shipped in the configuration sprint plan.) 10 min
6 M7 Fix AppConfig permission naming app_config.routes.ts:139-140 + seed/data/permission.ts Rename create_app_config.createapp_config.create and update_app_config.updateapp_config.update. Add the new strings to the permission seed. Cross-check with settings-configuration-sprint-plan.md AC-C1/AC-C2 — if already shipped there, mark this RESOLVED. 5 min
7 M8 Fix AppConfig hard-delete app_config.service.ts deleteAppConfig() Change configRepo.delete(id)configRepo.softDelete(id). Entity already has @DeleteDateColumn. Cross-check with configuration sprint plan AC-C3 — defer if already shipped. 1 min
8 H5 Strip backupCodes + verifyBackupCode from Setting entity/Setting.ts:39, setting.service.ts Remove backupCodes column and the verifyBackupCode() method (it references a non-existent userId column on the Setting entity — calling it would crash). Add a TypeORM migration to drop the column. The real backup-code logic lives on UserSetting. 15 min

Verification Checklist

After completing all 8 ship blockers:

  • GET /settings without an auth header returns 401 (or returns only the public allow-list)
  • Dashboard loads with <title> populated from admin.admin_site_title after a fresh yarn seed
  • Root layout generateMetadata() returns the seeded admin.meta_title (not the hardcoded fallback)
  • Footer-credit renders site link / site name / heart link from seed (no algorizetech.com hardcoded fallbacks)
  • With the API down, the dashboard still renders (no infinite <Loading fullScreen />); a toast surfaces the failure
  • POST /app-configs succeeds for a user with app_config.create permission
  • DELETE /app-configs/:id leaves the row in the DB with deletedAt set
  • Setting entity no longer compiles a reference to backupCodes or verifyBackupCode

Sprint 1: Make Settings State Coherent (Week 1)

Timeline: ~4 hours, 1 developer North Star: A setting edited in the admin UI is reflected in every consumer (root layout, dashboard, public SSR pages) without a server restart or stale cache. Exit criteria: Single source of truth for SSR settings access. Cache invalidation reaches every layer. No type coercion bugs between frontend fallbacks and seeded values.

Caching & Invalidation (1.5 hrs)

# Issue(s) Description File(s) Effort
S1-1 H2 RESOLVED 2026-04-13Delete fetchMetaData(), use shared getSettingByGroup. Root layout now imports getSettings from api/public/settings and calls it from generateMetadata(). fetchMetaData fully removed. app/layout.js:8,27-28 20 min
S1-2 H1 RESOLVED 2026-04-13 — Cache chain verified end-to-end using the stable Next.js 15 primitives: fetch() with next: { revalidate, tags } + revalidateTag(). Note: did NOT migrate to unstable_cache — that API is experimental and has known bugs. Current implementation tags group fetches with ["settings-group", "settings-<group>"] and mutations call revalidateBulk(["settings-group", "all-settings"]), which busts all grouped and all-settings cache entries in one call. Equivalent effect, stable API. app/layout.js, api/public/settings.js, actions/revalidate.js 30 min
S1-3 H3 RESOLVED 2026-04-13 (moot) — Originally required because sidebar-old.jsx consumed useSettingByGroup("site"). Both sidebar-old.jsx AND the useSettingByGroup hook itself have been deleted (see S1-8). No remaining consumers → group-scoped invalidation has nothing to bust. Revisit only if a grouped consumer is reintroduced. api/settings/index.js 15 min
S1-4 L3 RESOLVED 2026-04-13Replace deprecated keepPreviousData. useSettings() now uses placeholderData: keepPreviousData with proper import from @tanstack/react-query. (useSettingByGroup removed entirely in S1-8 follow-up.) api/settings/index.js:3,27 10 min

Type & Naming Consistency (1 hr)

# Issue(s) Description File(s) Effort
S1-5 M3 RESOLVED 2026-04-13Standardize website_type to string. register.jsx:52 now reads settings?.website_type \|\| "1". Backend strict equality with "2" now works correctly. register.jsx 15 min
S1-6 M5 RESOLVED 2026-04-13Standardize setting type field to "text". create-setting-form.jsx SETTING_TYPES contains { value: "text", label: "Text Input" } and the switch in setting-field.jsx handles "text" directly. Frontend side fully resolved; backend seed update is tracked under the configuration sprint plan. create-setting-form.jsx, setting-field.jsx 15 min
S1-7 M6 RESOLVED 2026-04-13Resolve getSettingByGroup name collision. Redux selector renamed to selectSettingByGroup in store/slices/settingSlice.js. All consumers (footer-credit.jsx, dashboard/layout.js) updated. SSR helper getSettingByGroup kept as the public API name. settingSlice.js, consumers 15 min
S1-8 M4 RESOLVED 2026-04-13Delete sidebar-old.jsx. File deleted. Follow-up: also deleted the now-orphaned useSettingByGroup hook from api/settings/index.js since no consumers remain. dashboard/components/sidebar-old.jsx (DELETED), api/settings/index.js (hook removed) 15 min

Sprint 1 Acceptance Test

1. yarn seed                           → Seeds all 25+ setting keys (incl. ship-blocker additions)
2. Login as admin                      → Dashboard loads
3. Edit "admin.meta_title" in Site Settings  → Save
4. Reload public homepage              → New title visible WITHOUT server restart
5. Edit "site.footer_copyright"        → Save
6. Reload public footer + dashboard footer → Both reflect new value
7. Edit a setting in the "site" group, then reload sidebar-old (if it existed)  → Would refetch (S1-3)
8. Register page load                  → website_type comparison works as string
9. yarn build                          → No React Query v5 deprecation warnings
10. Search codebase for "sidebar-old"  → No matches

Sprint 2: Clean Foundation (Week 2-3)

Timeline: ~6 hours, 1-2 developers North Star: Every consumer has a defined fallback. Dead code is gone. Documentation matches reality. First integration test exists for the Setting → SSR → consumer chain. Exit criteria: No empty renders on missing settings. Linting passes with no dead-code warnings. Architecture decision documented. One integration test green.

Frontend Polish (1 hr)

# Issue(s) Description Effort
S2-1 L1 RESOLVED 2026-04-13 — Add <Image> fallback in header.jsx. All 4 <Image> tags (site_logo + site_logo_dark, both pre-mount and post-mount branches) now use \|\| "/logo.png". /public/logo.png exists. 5 min
S2-2 L2 RESOLVED 2026-04-13 — Add footer_copyright fallback in footer-credit.jsx. Falls back to "All rights reserved." when undefined; surrounding markup already renders © {year} + site link. 5 min
S2-3 L7 RESOLVED 2026-04-13 — Replaced window.location.href in login.jsx (3 sites: JWT-from-URL effect, normal onSubmit, 2FA handler) with router.push() via a new shared useFinalizeLogin() hook in api/auth/index.js. Uncovered race condition: the original window.location.href path was masking a bug where useLogin.onSuccess seeded queryClient.setQueryData(["user"], data.data.user) with an incomplete user shape (no permissions); with useProfile's staleTime: 5 * 60 * 1000, React Query returned the stale seeded user and skipped the real /users/profile call. useFinalizeLogin fixes this by explicitly calling authAPI.getProfile(), seeding both Redux and RQ with the authoritative profile, checking dashboard.view permission, and routing to /dashboard or /dashboard/settings/profile accordingly. On failure: clears cookie, dispatches logout, routes to /login. Also applied to useLogout (router.push("/login")). 15 min → 45 min (due to race-condition fix)
S2-4 M1 RESOLVED 2026-04-13settinReducer typo already fixed in store/index.js (imports and uses settingReducer). Confirmed via cross-check with configuration sprint plan FE-L3. 2 min
S2-5 M2 / L5 RESOLVED 2026-04-13 — Console error messages in api/public/settings.js are adequate and unambiguous: "Failed to fetch settings group:" (L15), "Error fetching settings group:" (L30), "Failed to fetch settings:" (L54). Original sprint wording assumed a different code shape; current messaging is acceptable. 2 min
S2-6 L6 Fix updateSettingById controller success message. setting.controller.ts:79"Setting updated successfully". Cross-check with configuration sprint plan SET-M1 — defer if already shipped. 1 min

Backend Cleanup (45 min)

# Issue(s) Description Effort
S2-7 L4 Delete getImageSizeByKey() from setting.service.ts. Confirm zero call sites first. If a future feature needs max-image-size, it can be reintroduced via a normal getSettingByKey("admin.image_size") lookup. 10 min
S2-8 H5 follow-up After ship-blocker H5 lands, ship the migration: drop the backupCodes column from the settings table. Verify no production data depends on it. 30 min

Architecture & Documentation (2 hrs)

# Description Effort
S2-9 Document the Setting vs AppConfig vs UserSetting decision. Create docs/architecture-settings.md explaining: (a) Setting = public UI config (group.field key format, dot-split transform, dual SSR + CSR access); (b) AppConfig = server-side secrets, never exposed to frontend, encrypted; © UserSetting = per-user 2FA state. Include the data-flow diagram from the deep-dive section 10. 1 hr
S2-10 Document the public-GET decision (or undo it). Settings GET routes are now authenticated (ship-blocker C2). If SSR pages still need public access, document the public allow-list and the threat model in docs/security-settings.md. Tie the decision to the registration policy (auth.website_type) which currently leaks publicly. 30 min
S2-11 RESOLVED 2026-04-13 — Won't fix (premise disproved). Spike ran and found the Redux settingSlice is not a redundant mirror of React Query — it is the SSR hydration bridge for public/auth pages (populated by ThemeProvider from the prop passed by root app/layout.js) AND the live-update bridge for in-dashboard edits (populated by dashboard/layout.js on useSettings() refetch). Removing either dispatch introduces a real UX regression: dashboard consumers (footer-credit.jsx, theme-control.jsx) would show stale values until a hard page reload after an admin edit. TanStack Query alone cannot replace this because (a) useSettings() hits the authenticated endpoint and can't be called from public/auth pages, and (b) the public SSR helper getSettingByGroup() is server-only. An explanatory comment has been added to the setSettings dispatch in dashboard/layout.js so the non-obvious purpose isn't lost. Cross-reference with configuration sprint plan S2-22 — same conclusion applies there. (closed)

Testing (2 hrs)

# Description Effort
S2-12 First integration test for Settings. Set up Jest + ts-jest if not already done by the configuration sprint plan. Write one end-to-end test: seed a setting → call GET /settings/admin/prefix → assert the response has the expected dot-split shape → call PATCH /settings/:id → assert revalidateTag was called. This is the single most important regression preventer for the SSR cache chain. 2 hrs

Backlog (Ongoing)

Fix opportunistically when touching adjacent files. No dedicated sprint time needed.

Issue Description Fix When
L8 unstable_cache is labeled experimental in next/cache. Add a TODO comment and migrate when Next.js ships a stable replacement. Next.js stable cache API ships
Architecture Dual access pattern (Redux vs direct hook vs prop drill) for the same data. Resolved as intentional after S2-11 spike — the dual path is the SSR hydration + live-update bridge, not a redundancy. See S2-11 row. N/A
Architecture useSettingByGroup hook has zero remaining consumers after S1-8 (sidebar-old deleted). Consider deleting the hook itself in a follow-up cleanup. Next time api/settings/index.js is touched
SSR vs CSR Public SSR consumers (getSettingByGroup) and authenticated CSR consumer (useSettings) hit different endpoints with different cache keys. Acceptable today, document in S2-9. Documented in Sprint 2

Cross-Plan Overlap with settings-configuration-sprint-plan.md

This deep-dive partially overlaps with the earlier configuration sprint plan. Where the same issue is addressed in both, fix once and mark RESOLVED in both plans. Known overlaps:

This plan Configuration plan Status to verify
Ship #5 (C1 — non-blocking dashboard) FE-M7 (RESOLVED Apr 11b) Likely already shipped
Ship #6 (M7 — AppConfig perm naming) AC-C1 (Ship Blocker) Defer if shipped
Ship #6 seed addition AC-C2 (Ship Blocker) Defer if shipped
Ship #7 (M8 — AppConfig soft delete) AC-C3 (Ship Blocker) Defer if shipped
S1-7 (M6 — selector rename) FE-M6 (RESOLVED Apr 11b) Likely already shipped
S2-4 (M1 — settinReducer typo) FE-L3 Likely already shipped
S2-6 (L6 — controller message) SET-M1 Defer if shipped

Action item before starting: run git log --oneline docs/extra-docs/settings-configuration-sprint-plan.md and the actual code paths to mark the above as RESOLVED before beginning the ship-blocker work. Saves ~20 min of duplicate effort.


Key Decisions Log

Decision PM Engineer Designer Outcome
Fix order for ship blockers Security first (C2), then visible breakage (C1) Agree — C2 is 10 min, C1 is 10 min Dashboard infinite loading is the most user-visible Security → seed → dashboard → AppConfig
Public GET routes — keep or auth Auth them. Public allow-list only if SSR truly requires it. Easy to add allow-list mount SSR public pages need site/auth/contact groups Authenticate by default, add narrow public mount if needed
Seed missing keys vs add fallbacks Seed them — admin can edit later Seed is 5 min, fallbacks scattered Admin expectation is "edit, see change" Seed
Remove backupCodes from Setting Sprint 0 — dead column, no risk Sprint 0 — migration in Sprint 2 Invisible Ship blocker drop, Sprint 2 migration
Root layout unstable_cache migration Sprint 1 — fixes the "edit doesn't reflect" bug Sprint 1 — depends on H2 cleanup High user impact for admins Sprint 1
Redux removal Sprint 2 spike — share with configuration plan One spike covers both N/A Sprint 2 (single shared spike)
First integration test Sprint 2 — needs framework setup Sprint 2 — ~2 hrs N/A Sprint 2

Sprint Board Visualization

+-----------------------------------------------------------------+
|                    SHIP BLOCKERS (pre-release)                   |
|                    8 items . ~50 min . 1 developer              |
|                                                                  |
|  [SEC] C2 (auth GET)                                            |
|  [SEED] C3 C4 H4 (7 missing keys)                               |
|  [BUG] C1 (infinite loading)                                    |
|  [AC]  M7 (perm naming) M8 (soft delete) — defer if shipped    |
|  [DM]  H5 (backupCodes column drop)                             |
|                                                                  |
|  EXIT: Settings authenticated. Dashboard survives outage.        |
+-----------------------------+-----------------------------------+
                              v
+-----------------------------------------------------------------+
|                    SPRINT 1 -- "State Coherent" (week 1)         |
|                    8 items . ~4 hrs . 1 developer                |
|                                                                  |
|  [CACHE] H2 (delete fetchMetaData) H1 (unstable_cache) H3 (key) |
|  [DEPR]  L3 (placeholderData)                                    |
|  [TYPE]  M3 (website_type) M5 (text vs input)                   |
|  [NAME]  M6 (selector rename) M4 (delete sidebar-old)           |
|                                                                  |
|  EXIT: Edit -> save -> reload reflects everywhere.               |
+-----------------------------+-----------------------------------+
                              v
+-----------------------------------------------------------------+
|                    SPRINT 2 -- "Clean Foundation" (week 2-3)     |
|                    8 items + tests + docs . ~6 hrs               |
|                                                                  |
|  [FE]    L1 (header img) L2 (copyright) L7 (router)             |
|          M1 (typo) M2/L5 (log msg) L6 (msg)                     |
|  [BE]    L4 (dead method) H5 migration                          |
|  [DOCS]  S2-9 (architecture) S2-10 (public-GET decision)        |
|  [TEST]  Integration test (most important item)                 |
|                                                                  |
|  EXIT: Fallbacks everywhere. Docs match. Test passes.            |
+-----------------------------+-----------------------------------+
                              v
+-----------------------------------------------------------------+
|                    BACKLOG (ongoing)                              |
|                    1 item . fix when Next.js stabilizes          |
|                                                                  |
|  L8 (unstable_cache migration)                                  |
|  + dual-access-pattern simplification (covered by S2-11)        |
+-----------------------------------------------------------------+

Total Effort Estimate

Phase Items Effort Developers Timeline
Ship Blockers 8 (1 FE ✅, 7 BE open) ~50 min 1 Day 1 (before any deployment)
Sprint 1 8 (8 FE ✅, 0 open) ~4 hrs 1 Week 1
Sprint 2 8 + tests + docs (5 FE ✅, 3 BE/docs/tests open) ~6 hrs 1-2 Week 2-3
Backlog 1 N/A Ongoing
Total 25 (14 resolved 2026-04-13, 11 open) ~11 hrs (~6 hrs remaining) ~3 weeks

Resolved as of 2026-04-13: Ship #5 (C1), S1-1, S1-2, S1-3, S1-4, S1-5, S1-6, S1-7, S1-8, S2-1, S2-2, S2-3, S2-4, S2-5. All frontend work complete.

Still open: Backend ship blockers (C2, C3, C4, H4, M7, M8, H5), backend cleanup (S2-7, S2-8), architecture/docs (S2-9, S2-10, S2-11), testing (S2-12), backlog (L8).

Several items overlap with the Settings & Configuration Sprint Plan. Verify status before starting — overlap could trim ~30-45 min from the ship-blocker bucket.


Generated by BMAD Cross-Functional War Room workflow — Settings & AppConfig Data Flow, 2026-04-12 Cross-reference: Deep-Dive: Settings & AppConfig